Skip to content

Instantly share code, notes, and snippets.

@Jip-Hop
Last active February 25, 2024 14:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Jip-Hop/d82781da424724b4018bdfc5a2f1318b to your computer and use it in GitHub Desktop.
Save Jip-Hop/d82781da424724b4018bdfc5a2f1318b to your computer and use it in GitHub Desktop.
Simple comment preserving ConfigParser class to read/update/write INI files WITHOUT indented sections/keys/comments.

See the example usage inside configparser.py. Output when running the configparser.py file:

# Comments may appear before the first section
[Simple Values]
key = value
spaces in keys = allowed
spaces in values = allowed as well
spaces around the delimiter = obviously
you can also use = to delimit keys from values
# We will update some values in this section

[All Values Are Strings]
values like this = 2000000
or this = 2
# The comments will remain
are they treated as numbers? = no
# We can even use duplicate comments
# We can even use duplicate comments
integers, floats and booleans are held as = strings
# cOmMeNt cAsInG Is pReSeRvEd
can use the api to get converted values directly = true
// We can even use the // prefix to write comments

[Multiline Values]
# We can even use duplicate comments
chorus = I'm a lumberjack, and I'm okay
        I sleep all night and I work all day

[No Values]
key_without_value
empty string value here = 

[You can use comments]
# like this
; or this
# By default only in an empty line.
# Inline comments can be harmful because they prevent users
# from using the delimiting characters as parts of values.
# That being said, this can be customized.

[Sections Can NOT Be Indented]
can_values_be_as_well = False
multiline_values = #!/usr/bin/env bash
        set -euo pipefail

        # You can even write a little multiline bash script
        echo 'This script snippet is not mangled!'
        echo "added some $RANDOM stuff from python"
# Did I mention we can NOT indent comments, either?
# Indenting is reserved to handle multiline values
we can add keys = with some value
#!/usr/bin/env python3
import configparser
import io
import re
class CommentConfigParser(configparser.ConfigParser):
"""Comment preserving ConfigParser.
Limitation: No support for indenting section headers,
comments and keys. They should have no leading whitespace.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Backup _comment_prefixes
self._comment_prefixes_backup = self._comment_prefixes
# Unset _comment_prefixes so comments won't be skipped
self._comment_prefixes = ()
# Starting point for the comment IDs
self._comment_id = 0
# Default delimiter to use
delimiter = self._delimiters[0]
# Template to store comments as key value pair
self._comment_template = "#{0} " + delimiter + " {1}"
# Regex to match the comment prefix
self._comment_regex = re.compile(f"^#\d+\s*{re.escape(delimiter)}[^\S\n]*")
# Regex to match cosmetic newlines (skips newlines in multiline values):
# consecutive whitespace from start of line followed by a line not starting with whitespace
self._cosmetic_newlines_regex = re.compile(r"^(\s+)(?=^\S)", re.MULTILINE)
# List to store comments above the first section
self._top_comments = []
def _find_cosmetic_newlines(self, text):
# Indices of the lines containing cosmetic newlines
cosmetic_newline_indices = set()
for match in re.finditer(self._cosmetic_newlines_regex, text):
start_index = text.count("\n", 0, match.start())
end_index = start_index + text.count("\n", match.start(), match.end())
cosmetic_newline_indices.update(range(start_index, end_index))
return cosmetic_newline_indices
def _read(self, fp, fpname):
lines = fp.readlines()
cosmetic_newline_indices = self._find_cosmetic_newlines("".join(lines))
above_first_section = True
# Preprocess config file to preserve comments
for i, line in enumerate(lines):
if line.startswith("["):
above_first_section = False
elif above_first_section:
# Remove this line for now
lines[i] = ""
self._top_comments.append(line)
elif i in cosmetic_newline_indices or line.startswith(
self._comment_prefixes_backup
):
# Store cosmetic newline or comment with unique key
lines[i] = self._comment_template.format(self._comment_id, line)
self._comment_id += 1
# Feed the preprocessed file to the original _read method
return super()._read(io.StringIO("".join(lines)), fpname)
def write(self, fp, space_around_delimiters=True):
# Write the config to an in-memory file
with io.StringIO() as sfile:
super().write(sfile, space_around_delimiters)
# Start from the beginning of sfile
sfile.seek(0)
lines = sfile.readlines()
cosmetic_newline_indices = self._find_cosmetic_newlines("".join(lines))
for i, line in enumerate(lines):
if i in cosmetic_newline_indices:
# Remove newlines added below each section by .write()
lines[i] = ""
continue
# Remove the comment prefix (if regex matches)
lines[i] = self._comment_regex.sub("", line, 1)
fp.write("".join(self._top_comments + lines).rstrip())
def clear(self):
# Also clear the _top_comments
self._top_comments = []
super().clear()
# Example usage:
# Instantiate the comment preserving config parser
parser = CommentConfigParser(
interpolation=None,
allow_no_value=True,
comment_prefixes=("//", "#", ";"),
)
parser.read("./config")
section = parser["All Values Are Strings"]
section["values like this"] = str(
parser.getint("All Values Are Strings", "values like this") * 2
)
section["or this"] = "2"
section = parser["Sections Can NOT Be Indented"]
section["we can add keys"] = "with some value"
section["multiline_values"] += '\necho "added some $RANDOM stuff from python"'
# Print the modified config file to stdout with comments preserved
with io.StringIO() as sfile:
parser.write(sfile)
# Start from the beginning of sfile
sfile.seek(0)
print(sfile.read(), end="")
# Comments may appear before the first section
[Simple Values]
key=value
spaces in keys=allowed
spaces in values=allowed as well
spaces around the delimiter = obviously
you can also use : to delimit keys from values
# We will update some values in this section
[All Values Are Strings]
values like this: 1000000
or this: 3.14159265359
# The comments will remain
are they treated as numbers? : no
# We can even use duplicate comments
# We can even use duplicate comments
integers, floats and booleans are held as: strings
# cOmMeNt cAsInG Is pReSeRvEd
can use the API to get converted values directly: true
// We can even use the // prefix to write comments
[Multiline Values]
# We can even use duplicate comments
chorus: I'm a lumberjack, and I'm okay
I sleep all night and I work all day
[No Values]
key_without_value
empty string value here =
[You can use comments]
# like this
; or this
# 1 empty line above and 2 below, also preserved
# By default only in an empty line.
# Inline comments can be harmful because they prevent users
# from using the delimiting characters as parts of values.
# That being said, this can be customized.
[Sections Can NOT Be Indented]
can_values_be_as_well = False
multiline_values = #!/usr/bin/env bash
set -euo pipefail
# You can even write a little multiline bash script
echo 'This script snippet is not mangled!'
# Did I mention we can NOT indent comments, either?
# Indenting is reserved to handle multiline values
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment