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)