diff --git a/CHANGELOG b/CHANGELOG index b4520f2..ed4a3ac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +Unreleased +========== + +* preserve inline ``#`` and ``;`` in values unless preceded by whitespace when + using ``IniConfig.parse()``, including continuation lines (regarding #83) + 2.3.1 ===== diff --git a/README.rst b/README.rst index f0671ef..fcb238b 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,9 @@ If you have an ini file like this: line1 line2 -use ``IniConfig.parse()`` to strip inline comments from values: +use ``IniConfig.parse()`` to strip inline comments from values. Inline ``#`` and +``;`` start comments only when preceded by whitespace, so values such as URL +fragments and semicolon-separated text remain intact: .. code-block:: pycon diff --git a/src/iniconfig/__init__.py b/src/iniconfig/__init__.py index ddb97a3..49fbccd 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 ; preceded by whitespace starts + a comment. Other occurrences remain part of the value. 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..cc2090d 100644 --- a/src/iniconfig/_parse.py +++ b/src/iniconfig/_parse.py @@ -138,21 +138,24 @@ def _parseline( key_name = name.strip() # Strip value - 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_comment(value) - return key_name, value + return key_name, value.strip() # continuation else: - 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() - return None, line + line = _strip_inline_comment(line) + return None, line.strip() + + +def _strip_inline_comment(value: str) -> str: + for index, char in enumerate(value): + if index and char in COMMENTCHARS and value[index - 1].isspace(): + return value[:index] + return value def iscommentline(line: str) -> bool: diff --git a/testing/test_iniconfig.py b/testing/test_iniconfig.py index f0f1d9c..bbab824 100644 --- a/testing/test_iniconfig.py +++ b/testing/test_iniconfig.py @@ -316,6 +316,35 @@ def test_iscommentline_true(line: str) -> None: assert iscommentline(line) +@pytest.mark.parametrize("continuation", [False, True]) +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("http://example.org/#fragment", "http://example.org/#fragment"), + ("a;b", "a;b"), + ("a#b;c", "a#b;c"), + ("a#b ; comment", "a#b"), + ("a;b # comment", "a;b"), + ("a#b;still-value ; comment # later", "a#b;still-value"), + ("value\t# comment", "value"), + ("value\u00a0; comment", "value"), + ("value # comment; later", "value"), + ], +) +def test_parse_inline_comment_requires_whitespace( + value: str, expected: str, continuation: bool +) -> None: + assignment = f"key =\n {value}" if continuation else f"key = {value}" + config = IniConfig.parse("test.ini", data=f"[section]\n{assignment}") + assert config["section"]["key"] == expected + + +@pytest.mark.parametrize("value", ["#literal", ";literal"]) +def test_parse_comment_marker_immediately_after_separator(value: str) -> None: + config = IniConfig.parse("test.ini", data=f"[section]\nkey={value}") + assert config["section"]["key"] == value + + def test_parse_strips_inline_comments() -> None: """Test that IniConfig.parse() strips inline comments from values by default.""" config = IniConfig.parse( @@ -332,7 +361,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"