26

Consider the following INI file:

[TestSettings]
# First comment goes here
environment = test

[Browser]
# Second comment goes here
browser = chrome
chromedriver = default

...

I'm using Python 2.7 to update the ini file:

config = ConfigParser.ConfigParser()
config.read(path_to_ini)
config.set('TestSettings','environment',r'some_other_value')

with open(path_to_ini, 'wb') as configfile:
    config.write(configfile)

How can I update the INI file without removing the comments. The INI file is updated but the comments are removed.

[TestSettings]
environment = some_other_value

[Browser]
browser = chrome
chromedriver = default
sarbo
  • 1,661
  • 6
  • 21
  • 26

5 Answers5

24

The reason that comments in config files are wiped when writing back is that the write method didn't take care of comments at all. It just writes key/value pairs.

The easiest way to bypass this is to init configparser object with a customized comment prefix and allow_no_value = True. Then, if we want to keep the default "#" and ";" as comment lines in the file, we can specify another comment prefix, like "/" with comment_prefixes='/'. You can read this section of the configparser documentation for further information.

i.e., to keep comments, you have to trick configparser into believing that lines starting with "#" are not comments, but they are keys without a value. Interesting :)

# set comment_prefixes to a string which you will not use in the config file
config = configparser.ConfigParser(comment_prefixes='/', allow_no_value=True)
config.read_file(open('example.ini'))
...
config.write(open('example.ini', 'w'))
Aelius
  • 1,029
  • 11
  • 22
wang yi
  • 329
  • 2
  • 5
  • Unofrtunately, this doesn't work for me: ```cp = ConfigParser.ConfigParser(allow_no_value=True, comment_prefixes='/') TypeError: __init__() got an unexpected keyword argument 'comment_prefixes'``` maybe this works only with newer versions of Configparser – Mino_e Dec 17 '18 at 11:38
  • Ok so i just tried it out with python3, there it works. With python2 the error message above will be printed – Mino_e Dec 17 '18 at 12:53
  • This doesn't help with commented lines, they're still deleted – hryamzik Jun 21 '19 at 11:04
  • This ended up making all my comments lower case – eric.frederich Aug 07 '20 at 13:24
  • @eric.frederich This is because there is a default converter to `configparser`. You can bypass it by doing `config.optionxform = lambda option: option`. See [this link](https://docs.python.org/3/library/configparser.html#configparser.ConfigParser.BOOLEAN_STATES) – Tomerikoo Aug 26 '20 at 07:07
  • Nice workaround. For me it first still wiped certain comments cause I used bulk editing of sections: `config["SECTION"] = {"key1": "value1", "key2": "value2"}`. So keep in mind to always just set key value pairs individually. – rayon May 15 '21 at 18:12
  • 1
    This doesn't work very well at all - it fails for duplicate comments, and comments at the top of the file. – Addison Jan 12 '22 at 00:13
11

ConfigObj preserves comments when reading and writing INI files, and seems to do what you want. Example usage for the scenario you describe :

from configobj import ConfigObj

config = ConfigObj(path_to_ini)
config['TestSettings']['environment'] = 'some_other_value'
config.write()
Lex Scarisbrick
  • 1,540
  • 1
  • 24
  • 31
2

ConfigUpdater can update .ini files and preserve comments: pyscaffold/configupdater.

I don't know if it works for Python 2 though.

From the docs:

The key differences to ConfigParser are:

  • minimal invasive changes in the update configuration file,
  • proper handling of comments,
andreoliwa
  • 156
  • 2
0

ConfigObj is the best option in almost all cases.

Nevertheless, it does not support multiline values without triple quotes, like ConfigParser do. In this case, a viable option can be iniparse.

For example:

[TestSettings]
# First comment goes here
multiline_option = [
        first line,
        second line,
    ]

You can update the multiline value in this way.

import iniparse
import sys

c = iniparse.ConfigParser()
c.read('config.ini')
value = """[
    still the first line,
    still the second line,
]
"""
c.set('TestSettings', 'multiline_option', value=value)
c.write(sys.stdout)
shishax
  • 51
  • 1
  • 4
0

Unless the configparser changes their implementation, all items not in option and section will not be read, so that when you write it back, un-read item is lost. You may write your update as follows:

def update_config(file, section, option, value, comment: str = None):
    sectFound = False
    lineIdx = 0
    with open(file, 'r') as config:
        lines = config.readlines()
        lineCount = len(lines)
        for line in lines:
            lineIdx += 1
            if sectFound and line.startswith('['):  #next secion
                lineIdx += -1
                lines.insert(lineIdx, option + ' = ' + value)
                if comment is not None:
                    lineIdx += 1
                    lines.insert(lineIdx, option + ' = ' + comment)
                break
            elif sectFound and line.startswith(option + ' = '):
                lines.pop(lineIdx)
                lines.insert(lineIdx, option + ' = ' + value)
                if comment is not None:
                    lineIdx += 1
                    lines.insert(lineIdx, option + ' = ' + comment)
                break
            elif sectFound and lineIdx == lineCount:
                lineIdx += 1
                lines.insert(lineIdx, option + ' = ' + value + '\n')
                if comment is not None:
                    lineIdx += 1
                    lines.insert(lineIdx, comment + '\n')
                break
            if line.strip() == '[' + section + ']':
                sectFound = True
    with open(file, 'w') as cfgfile:
        cfgfile.writelines(lines)
        if sectFound == False:
            cfgfile.writelines('[' + section + ']\n' + option + ' = ' + value)
            if comment is not None:
                cfgfile.writelines(comment)