0

Is it possible that ConfigParser keeps the format of INI config file? I have config files which have comments and specific section/option names and if a read and change the content of file the ConfigParser re-format it (I can solve the section/option names).

I am familiar with the way of working of ConfigParser (Read key/value pairs to a dict and dumping it to the file after change). But I am interested if there is solution to keep the original format and comments in the INI file.

Example:

test.ini

# Comment line
; Other Comment line
[My-Section]
Test-option = Test-Variable

test.py

import configparser as cp

parser: cp.ConfigParser = cp.ConfigParser()
parser.read("test.ini")

parser.set("My-Section", "New-Test_option", "TEST")

with open("test.ini", "w") as configfile:
    parser.write(configfile)

test.ini after run script

[My-Section]
test-option = Test-Variable
new-test_option = TEST

As you can see above the comment lines (both types of comments) have been removed. Furthermore, the option names have been re-formatted.

If I add the following line to source code then I can keep the format of the options but the comments are still removed:

parser.optionxform = lambda option: option

So the test.ini file after run the script with above line:

[My-Section]
Test-option = Test-Variable
New-Test_option = TEST

So my question(s):

  • Is it possible to keep the comments in the INI file after change it?
  • Is it possible to keep the formatting of file eg.: spaces, tabs, new lines etc...?

Note:

  • I have already checked the RawConfigParser module but as I saw that also doesn't support the format keeping.
milanbalazs
  • 4,811
  • 4
  • 23
  • 45
  • The docs state, `Note Comments in the original configuration file are not preserved when writing the configuration back.`. – wwii Jan 19 '23 at 14:18
  • 1
    If someone hasn't already done it, you will probably have to subclass ConfigParser and modify it to keep track of comments and where they belong. – wwii Jan 19 '23 at 14:22
  • Does [Writing comments to files with ConfigParser](https://stackoverflow.com/questions/6620637/writing-comments-to-files-with-configparser) answer your question? [Update INI file without removing comments](https://stackoverflow.com/questions/21476554/update-ini-file-without-removing-comments). – wwii Jan 19 '23 at 14:25
  • Thanks for your comments. The linked SO question/answers are not really solutions for me because I have comments outside of `sections`, furthermore I want to keep the existing comments in the `INI` file and do not add new ones. Probably you are right, I have to write and own (extend the `ConfigParser` module) parser... I hoped somebody has already faced with this issue and made (found) something solution for it. :) – milanbalazs Jan 19 '23 at 14:30

1 Answers1

0

From the docs:

Comments in the original configuration file are not preserved when writing the configuration back.

IF comments are always before section names you could preprocess and post process the file to capture comments and restore them after the changes were made. This is a bit of a hack but maybe easier to implement than extending configparser.ConfigParser.

Does not account for in-line comments although the same method could be employed to find, associate, and restore inline comments.

Use a regular expression to find comments then associate them with sections.

import configparser as cp
import re

pattern = r'([#;][^[]*)(\[[^]]*\])'
rex = re.compile(pattern)
# {section_name:comment}
comments = {}
with open('test.ini') as f:
    text = f.read()
for comment,section in rex.findall(text):
    comments[section] = comment

Make the changes with ConfigParser.

parser: cp.ConfigParser = cp.ConfigParser()
parser.read("test.ini")

parser.set("My-Section", "New-Test_option", "TEST")

with open("test.ini", "w") as configfile:
    parser.write(configfile)

Restore the comments.

with open('test.ini') as f:
    text = f.read()
for section,comment in comments.items():
    text = text.replace(section,comment+section)
with open('test.ini','w') as f:
    f.write(text)

test.ini

# comment line
; other comment line
[My-Section]
test-option = Test-Variable

; another pesky comment

[foo-section]
this-option = x

The comments dict looks like:

{'[My-Section]': '# comment line\n; other comment line\n',
 '[foo-section]': '; another pesky comment\n\n'}

test.ini after changes

# comment line
; other comment line
[My-Section]
test-option = Test-Variable
new-test_option = TEST

; another pesky comment

[foo-section]
this-option = x

Finally here is a subclass of ConfigParser with the _read and _write_section methods overridden to find/associate/restore comments IF they appear just before sections.

import configparser as cp
from configparser import *
import re
class ConfigParserExt(cp.ConfigParser):
    def _read(self, fp, fpname):
        """Parse a sectioned configuration file.
        Each section in a configuration file contains a header, indicated by
        a name in square brackets (`[]`), plus key/value options, indicated by
        `name` and `value` delimited with a specific substring (`=` or `:` by
        default).
        Values can span multiple lines, as long as they are indented deeper
        than the first line of the value. Depending on the parser's mode, blank
        lines may be treated as parts of multiline values or ignored.
        Configuration files may include comments, prefixed by specific
        characters (`#` and `;` by default). Comments may appear on their own
        in an otherwise empty line or may be entered in lines holding values or
        section names. Please note that comments get stripped off when reading configuration files;
        unless they are positioned just before sections
        """

        # find comments and associate with section
        try:
            text = fp.read()
            fp.seek(0)
        except AttributeError as e:
            text = ''.join(line for line in fp)
        rex = re.compile(r'([#;][^[]*)(\[[^]]*\])')
        self.preserved_comments = {}
        for comment,section in rex.findall(text):
            self.preserved_comments[section] = comment
        
        super()._read(fp,fpname)

    def _write_section(self, fp, section_name, section_items, delimiter):
        """Write a single section to the specified `fp`."""
        # restore comment to section
        if f'[{section_name}]' in self.preserved_comments:
            fp.write(self.preserved_comments[f'[{section_name}]'])
        super()._write_section( fp, section_name, section_items, delimiter)
wwii
  • 23,232
  • 7
  • 37
  • 77