Skip to content

Instantly share code, notes, and snippets.

@Jip-Hop
Last active July 6, 2025 02:44
Show Gist options
  • 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.

Revisions

  1. Jip-Hop revised this gist Jun 29, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion commentconfigparser.py
    Original file line number Diff line number Diff line change
    @@ -24,7 +24,7 @@ def __init__(self, *args, **kwargs):
    # 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]*")
    self._comment_regex = re.compile(r"^#\d+\s*" + re.escape(delimiter) + r"[^\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)
  2. Jip-Hop revised this gist Jun 29, 2024. 1 changed file with 7 additions and 1 deletion.
    8 changes: 7 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -2,14 +2,15 @@ See the example usage inside `configparser.py`. Output when running the `configp

    ```
    # 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
    # We will update some values in this section
    [All Values Are Strings]
    values like this = 2000000
    or this = 2
    @@ -34,6 +35,10 @@ 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.
    @@ -47,6 +52,7 @@ multiline_values = #!/usr/bin/env bash
    # 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
  3. Jip-Hop revised this gist Feb 25, 2024. 1 changed file with 4 additions and 2 deletions.
    6 changes: 4 additions & 2 deletions commentconfigparser.py
    Original file line number Diff line number Diff line change
    @@ -19,10 +19,12 @@ def __init__(self, *args, **kwargs):
    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} = {1}"
    self._comment_template = "#{0} " + delimiter + " {1}"
    # Regex to match the comment prefix
    self._comment_regex = re.compile(r"^#\d+\s*=[^\S\n]*")
    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)
  4. Jip-Hop revised this gist Feb 25, 2024. 1 changed file with 5 additions and 2 deletions.
    7 changes: 5 additions & 2 deletions commentconfigparser.py
    Original file line number Diff line number Diff line change
    @@ -17,6 +17,8 @@ def __init__(self, *args, **kwargs):
    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
    # Template to store comments as key value pair
    self._comment_template = "#{0} = {1}"
    # Regex to match the comment prefix
    @@ -53,8 +55,9 @@ def _read(self, fp, fpname):
    elif i in cosmetic_newline_indices or line.startswith(
    self._comment_prefixes_backup
    ):
    # Store cosmetic newline or comment with unique key based on line index
    lines[i] = self._comment_template.format(i, line)
    # 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)
  5. Jip-Hop revised this gist Feb 25, 2024. 1 changed file with 5 additions and 0 deletions.
    5 changes: 5 additions & 0 deletions commentconfigparser.py
    Original file line number Diff line number Diff line change
    @@ -79,6 +79,11 @@ def write(self, fp, space_around_delimiters=True):

    fp.write("".join(self._top_comments + lines).rstrip())

    def clear(self):
    # Also clear the _top_comments
    self._top_comments = []
    super().clear()


    # Example usage:

  6. Jip-Hop revised this gist Feb 24, 2024. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions commentconfigparser.py
    Original file line number Diff line number Diff line change
    @@ -21,9 +21,9 @@ def __init__(self, *args, **kwargs):
    self._comment_template = "#{0} = {1}"
    # Regex to match the comment prefix
    self._comment_regex = re.compile(r"^#\d+\s*=[^\S\n]*")
    # Regex to match cosmetic newlines (to filter out newlines in multiline values):
    # consecutive empty lines followed by a line which does not start with whitespace
    self._cosmetic_newlines_regex = re.compile(r"^(\s*\n)+(?=\S)", re.MULTILINE)
    # 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 = []

  7. Jip-Hop revised this gist Feb 24, 2024. 2 changed files with 38 additions and 12 deletions.
    46 changes: 34 additions & 12 deletions commentconfigparser.py
    100644 → 100755
    Original file line number Diff line number Diff line change
    @@ -19,26 +19,42 @@ def __init__(self, *args, **kwargs):
    self._comment_prefixes = ()
    # Template to store comments as key value pair
    self._comment_template = "#{0} = {1}"
    # Regex to match the comment id prefix
    self._comment_regex = re.compile(r"^#\d+\s*=\s*")
    # Regex to match the comment prefix
    self._comment_regex = re.compile(r"^#\d+\s*=[^\S\n]*")
    # Regex to match cosmetic newlines (to filter out newlines in multiline values):
    # consecutive empty lines followed by a line which does not start with whitespace
    self._cosmetic_newlines_regex = re.compile(r"^(\s*\n)+(?=\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 line.startswith(self._comment_prefixes_backup):
    if above_first_section:
    # Remove this line for now
    lines[i] = ""
    self._top_comments.append(line)
    else:
    # Store comment as value with unique key based on line number
    lines[i] = self._comment_template.format(i, line)
    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 based on line index
    lines[i] = self._comment_template.format(i, line)

    # Feed the preprocessed file to the original _read method
    return super()._read(io.StringIO("".join(lines)), fpname)
    @@ -51,11 +67,17 @@ def write(self, fp, space_around_delimiters=True):
    sfile.seek(0)
    lines = sfile.readlines()

    cosmetic_newline_indices = self._find_cosmetic_newlines("".join(lines))

    for i, line in enumerate(lines):
    # Remove the comment id prefix
    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))
    fp.write("".join(self._top_comments + lines).rstrip())


    # Example usage:
    4 changes: 4 additions & 0 deletions config
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,5 @@
    # Comments may appear before the first section

    [Simple Values]
    key=value
    spaces in keys=allowed
    @@ -32,6 +33,9 @@ empty string value here =
    # 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.
  8. Jip-Hop renamed this gist Feb 24, 2024. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  9. Jip-Hop renamed this gist Feb 24, 2024. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  10. Jip-Hop created this gist Feb 22, 2024.
    53 changes: 53 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,53 @@
    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
    ```
    49 changes: 49 additions & 0 deletions config
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,49 @@
    # 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

    # 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
    86 changes: 86 additions & 0 deletions configparser.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,86 @@
    #!/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 = ()
    # Template to store comments as key value pair
    self._comment_template = "#{0} = {1}"
    # Regex to match the comment id prefix
    self._comment_regex = re.compile(r"^#\d+\s*=\s*")
    # List to store comments above the first section
    self._top_comments = []

    def _read(self, fp, fpname):
    lines = fp.readlines()
    above_first_section = True
    # Preprocess config file to preserve comments
    for i, line in enumerate(lines):
    if line.startswith("["):
    above_first_section = False
    elif line.startswith(self._comment_prefixes_backup):
    if above_first_section:
    # Remove this line for now
    lines[i] = ""
    self._top_comments.append(line)
    else:
    # Store comment as value with unique key based on line number
    lines[i] = self._comment_template.format(i, line)

    # 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()

    for i, line in enumerate(lines):
    # Remove the comment id prefix
    lines[i] = self._comment_regex.sub("", line, 1)

    fp.write("".join(self._top_comments + lines))


    # 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="")