Search code examples
python-3.xconfigparser

In ConfigParser how to get only section-local options?


Consider the following MWE named foo.py:

#!/usr/bin/env python3
from configparser import ConfigParser

if __name__ == "__main__":
    cfg = ConfigParser()
    with open("foo.ini") as ini:
        cfg.read_file(ini)
    for section in cfg.sections():
        print("[%s]" % (section))
        for option, value in cfg.items(section):
            print("%s = %s" % (option, value))

... and the accompanying foo.ini:

[DEFAULT]
basedir = /foo/bar
sourcedir = %(basedir)s/baz

[task:1]
sourcedir = /usr
workdir = %(sourcedir)s/src

[task:2]
hdrdir = %(sourcedir)s/include

The script will yield the following when run:

[task:1]
basedir = /foo/bar
sourcedir = /usr
workdir = /usr/src
[task:2]
basedir = /foo/bar
sourcedir = /foo/bar/baz
hdrdir = /foo/bar/baz/include

The desired outcome is to iterate exclusively over the (interpolated) options (via ConfigParser.items()) local to the given section.

Passing raw=True to ConfigParser.items() doesn't affect the list of items other than that the values are not being interpolated (which is anyway not the desired outcome).

Now I could do something like in this modified version of the initial script:

#!/usr/bin/env python3
from configparser import ConfigParser

if __name__ == "__main__":
    cfg = ConfigParser()
    with open("foo.ini") as ini:
        cfg.read_file(ini)
    print("; sections: %s" % (cfg.sections()))
    print("; defaults: %s" % (cfg.defaults()))
    for section in cfg.sections():
        print("[%s]" % (section))
        print("; options = %s" % (cfg.options(section)))
        for option, value in cfg.items(section):
            if option in cfg.defaults():
                # can't use cfg.defaults()[option] ... that's the raw value
                if cfg[cfg.default_section][option] == value:
                    continue
            print("%s = %s" % (option, value))

... which yields:

; sections: ['task:1', 'task:2']
; defaults: OrderedDict([('basedir', '/foo/bar'), ('sourcedir', '%(basedir)s/baz')])
[task:1]
; options = ['sourcedir', 'workdir', 'basedir']
sourcedir = /usr
workdir = /usr/src
[task:2]
; options = ['hdrdir', 'basedir', 'sourcedir']
hdrdir = /foo/bar/baz/include

So it'll work at first glance, but would filter out section-local options which have a value identical to the global one of the same name.

Now this may seem only like a cosmetic issue, but in my case there is a semantic difference, still, between whether the option exists locally inside a section or whether its value was inherited from the default section (think multiple files, multiple inherited options ...). Point being that this is a naïve approach which doesn't work for me.

So the question: how can I get just the options that are really inside a section of an .ini file, without all those global inherited options? Is there a way to do this without reaching into the guts of the configparser module and tampering with non-public methods and stuff?


Solution

  • Inspecting the code of ConfigParser I don't see a public API to do it. Indeed, any method in ConfigParser extends the given section with defaults.

    The only straightfoward way I see would be to change the default section either in your config or in ConfigParser constructor (default_section argument):

    if __name__ == "__main__":
        cfg = ConfigParser(default_section=None)
    # (...)
    

    However the side effect is that the automatic "default" mechanism will not work any more for this instanciated ConfigParser. However you can still do the fallback mechanism your self:

    cfg = ConfigParser(default_section=None)
    cfg.get(mysection, myoption, fallback=cfg.get('DEFAULT', myoption))
    

    Also, the interpolation in values will not work from DEFAULT section. But you can use ExtendedInterpolation.

    Full example:

    # test.py
    from configparser import ConfigParser, DEFAULTSECT, ExtendedInterpolation
    
    if __name__ == '__main__':
        cfg = ConfigParser(default_section=None, interpolation=ExtendedInterpolation())
    
        with open('test.ini') as ini:
            cfg.read_file(ini)
    
        for section in cfg.sections():
            if section == DEFAULTSECT:
                continue
    
            print('[%s]' % (section))
            for key, value in cfg.items(section):
                print('%s = %s' % (key, value))
    
    # test.ini
    [DEFAULT]
    foo=bar
    
    [section]
    key="value ${DEFAULT:foo}"
    

    Edits:

    • Rename DEFAULT section is not a good idea if the purpose of it is to configure default values. I strike this option.
    • Set default_section as None instead of "whatever"
    • Add a full code example
    • Add details about ExtendedInterpolation