pythoniniconfigparser

ConfigParser - write a list as a duplicate key


I have searched stackoverflow but can't seem to find an answer similar to mine.

Most people want to parse duplicate keys, however I want to write them. (I'd prefer to do this using async file writes but that's another story)

Specifically for systemd-networkd config. They use the ini format, but allow duplicate keys. For example

[Match]
Name=eth0

[Network]
Address=10.0.0.12/24
Gateway=10.0.0.1
DNS=1.1.1.1
DNS=8.8.8.8

I want to be able to write this like so:

conf = ConfigParser()
conf.optionxform = str

conf["Match"] = {"Name": "eth0"}
conf["Network"] = {
    "DHCP": "no",
    "Address": "10.0.0.12/24",
    "Gateway": "10.0.0.1",
    "Dns": ["1.1.1.1", "8.8.8.8"],
}

However, obviously, this gets written as a literal string.

I've written the following abomination that does the job, but surely there has to be an easier / more sane way?

from configparser import ConfigParser
import ast

class CustomParser(ConfigParser):
    def _write_section(self, fp, section_name, section_items, delimiter):
        """Write a single section to the specified `fp`."""

        def write_item(item):
            item = self._interpolation.before_write(
                self, section_name, key, item
            )
            if item is not None or not self._allow_no_value:
                item = delimiter + str(item).replace("\n", "\n\t")
            else:
                item = ""
            fp.write("{}{}\n".format(key, item))

        fp.write("[{}]\n".format(section_name))
        for key, value in section_items:
            parsed = None
            try:
                parsed = ast.literal_eval(value)
            except Exception:
                pass

            if isinstance(parsed, list):
                for list_item in parsed:
                    write_item(list_item)
            else:
                write_item(value)
        fp.write("\n")

Solution

  • One solution would be create custom dict and initialize ConfigParser() with that dict (dict_type= parameter in constructor):

    import sys
    from ast import literal_eval
    from configparser import ConfigParser
    
    
    class my_dict(dict):
        def items(self):
            for k, v in super().items():
                if v.startswith("[") and v.endswith("]"):
                    for i in literal_eval(v):
                        yield k, i
                else:
                    yield k, v
    
    
    conf = ConfigParser(dict_type=my_dict)
    
    conf["Match"] = {"Name": "eth0"}
    conf["Network"] = {
        "DHCP": "no",
        "Address": "10.0.0.12/24",
        "Gateway": "10.0.0.1",
        "Dns": ["1.1.1.1", "8.8.8.8"],
    }
    
    
    conf.write(sys.stdout)
    

    Prints:

    [Match]
    name = eth0
    
    [Network]
    dhcp = no
    address = 10.0.0.12/24
    gateway = 10.0.0.1
    dns = 1.1.1.1
    dns = 8.8.8.8