From 32cd4bd937150cbe55c6a56cf269c328daf52524 Mon Sep 17 00:00:00 2001 From: kaixin liu Date: Fri, 9 Oct 2026 01:18:29 +0800 Subject: [PATCH] Preserve embedded comment characters in parsed INI values --- README.rst | 5 +++++ src/iniconfig/__init__.py | 4 ++-- src/iniconfig/_parse.py | 13 +++++++++---- testing/test_iniconfig.py | 36 +++++++++++++++++++++++++++++++++++- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index f0671ef..6c6b181 100644 --- a/README.rst +++ b/README.rst @@ -56,3 +56,8 @@ use ``IniConfig.parse()`` to strip inline comments from values: The ``IniConfig()`` constructor preserves inline comments in values for backward compatibility. Use ``IniConfig.parse(..., strip_inline_comments=False)`` to preserve them when using the parsing method. + +When inline comment stripping is enabled, ``#`` and ``;`` start a comment only +at the beginning of a value or after whitespace. Characters within a value, +such as the ``#`` in ``https://example.com/#fragment`` or the ``;`` in ``a;b``, +are preserved. The same rule applies to continuation lines. diff --git a/src/iniconfig/__init__.py b/src/iniconfig/__init__.py index ddb97a3..250c762 100644 --- a/src/iniconfig/__init__.py +++ b/src/iniconfig/__init__.py @@ -139,8 +139,8 @@ def parse( data: Optional INI content as string. If None, reads from path. encoding: Encoding to use when reading the file (default: utf-8) strip_inline_comments: Whether to strip inline comments from values - (default: True). When True, comments starting with # or ; are - removed from values, matching the behavior for section comments. + (default: True). When True, # or ; at the beginning of a value + or after whitespace starts a comment, including on continuation lines. strip_section_whitespace: Whether to strip whitespace from section and key names (default: False). When True, strips Unicode whitespace from section and key names, addressing issue #4. When False, preserves existing behavior for backward compatibility. diff --git a/src/iniconfig/_parse.py b/src/iniconfig/_parse.py index a24b257..91bb993 100644 --- a/src/iniconfig/_parse.py +++ b/src/iniconfig/_parse.py @@ -141,8 +141,7 @@ def _parseline( value = value.strip() # Strip inline comments from values if requested (issue #55) if strip_inline_comments: - for c in COMMENTCHARS: - value = value.split(c)[0].rstrip() + value = _strip_inline_comments(value) return key_name, value # continuation @@ -150,11 +149,17 @@ def _parseline( line = line.strip() # Strip inline comments from continuations if requested (issue #55) if strip_inline_comments: - for c in COMMENTCHARS: - line = line.split(c)[0].rstrip() + line = _strip_inline_comments(line) return None, line +def _strip_inline_comments(value: str) -> str: + for index, char in enumerate(value): + if char in COMMENTCHARS and (index == 0 or value[index - 1].isspace()): + return value[:index].rstrip() + return value + + def iscommentline(line: str) -> bool: c = line.lstrip()[:1] return c in COMMENTCHARS diff --git a/testing/test_iniconfig.py b/testing/test_iniconfig.py index f0f1d9c..487bbca 100644 --- a/testing/test_iniconfig.py +++ b/testing/test_iniconfig.py @@ -332,7 +332,7 @@ def test_parse_strips_inline_comments() -> None: ) assert config["section1"]["name1"] == "value1" assert config["section1"]["name2"] == "value2" - assert config["section1"]["name3"] == "value3" + assert config["section1"]["name3"] == "value3# no space before comment" assert config["section1"]["list"] == "a, b, c" @@ -353,6 +353,40 @@ def test_parse_strips_inline_comments_from_continuations() -> None: assert config["section"]["names"] == "Alice\nBob\nCharlie" +@pytest.mark.parametrize("continuation", [False, True]) +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("http://example.com/#fragment", "http://example.com/#fragment"), + ("a#b", "a#b"), + ("d;e", "d;e"), + ("a#b ; comment", "a#b"), + ("d;e # comment", "d;e"), + ("value # comment;more", "value"), + ("value ; comment#more", "value"), + ("value\t# comment", "value"), + ("value\u00a0; comment", "value"), + ("value#", "value#"), + ("value;", "value;"), + ], +) +def test_parse_inline_comment_boundaries( + value: str, expected: str, continuation: bool +) -> None: + if continuation: + data = f"[section]\nkey =\n {value}\n" + else: + data = f"[section]\nkey = {value}\n" + config = IniConfig.parse("test.ini", data=data) + assert config["section"]["key"] == expected + + +@pytest.mark.parametrize("value", ["# comment", "; comment"]) +def test_parse_comment_only_value(value: str) -> None: + config = IniConfig.parse("test.ini", data=f"[section]\nkey = {value}\n") + assert config["section"]["key"] == "" + + def test_parse_preserves_inline_comments_when_disabled() -> None: """Test that IniConfig.parse(strip_inline_comments=False) preserves comments.""" config = IniConfig.parse(