From ad631099729021b9711fb160d5c7e2cbb6a6463b Mon Sep 17 00:00:00 2001 From: Pitchfork-and-Torch Date: Fri, 18 Sep 2026 02:15:38 +0000 Subject: [PATCH 1/2] Strip leading UTF-8 BOM when reading INI files Windows editors often write a UTF-8 BOM; with encoding=utf-8 that left U+FEFF on the first section line and raised ParseError. Strip the BOM after read for both IniConfig() and IniConfig.parse(). --- src/iniconfig/__init__.py | 6 ++++++ testing/test_iniconfig.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/iniconfig/__init__.py b/src/iniconfig/__init__.py index b84809f..e9fd907 100644 --- a/src/iniconfig/__init__.py +++ b/src/iniconfig/__init__.py @@ -112,6 +112,9 @@ def __init__( if data is None: with open(self.path, encoding=encoding) as fp: data = fp.read() + # Strip a leading UTF-8 BOM so editors that write one still parse. + if data.startswith(""): + data = data.removeprefix("") # Use old behavior (no stripping) for backward compatibility sections_data, sources = _parse.parse_ini_data( @@ -166,6 +169,9 @@ def parse( if data is None: with open(fspath, encoding=encoding) as fp: data = fp.read() + # Strip a leading UTF-8 BOM so editors that write one still parse. + if data.startswith(""): + data = data.removeprefix("") sections_data, sources = _parse.parse_ini_data( fspath, diff --git a/testing/test_iniconfig.py b/testing/test_iniconfig.py index 85193c5..488178f 100644 --- a/testing/test_iniconfig.py +++ b/testing/test_iniconfig.py @@ -412,3 +412,23 @@ def test_unicode_whitespace_in_key_names() -> None: ) assert "key" in config["section"] assert config["section"]["key"] == "value" + + +def test_utf8_bom_file(tmp_path): + """Files saved with a UTF-8 BOM (common on Windows editors) must parse.""" + path = tmp_path / "bom.ini" + path.write_bytes(b"\xef\xbb\xbf[section]\nkey = value\n") + config = IniConfig(path) + assert config["section"]["key"] == "value" + + +def test_utf8_bom_in_data_string(): + config = IniConfig("x.ini", data="\ufeff[section]\nkey = value\n") + assert config["section"]["key"] == "value" + + +def test_parse_utf8_bom_file(tmp_path): + path = tmp_path / "bom.ini" + path.write_bytes(b"\xef\xbb\xbf[section]\nkey = value\n") + config = IniConfig.parse(path) + assert config["section"]["key"] == "value" From 270e11952c01dbec408f5e636f1177d56cb9b912 Mon Sep 17 00:00:00 2001 From: Pitchfork-and-Torch <297513015+Pitchfork-and-Torch@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:23:34 -0400 Subject: [PATCH 2/2] Move UTF-8 BOM stripping into parse_ini_data Strip once in parse_ini_data, name the mark with \N{BYTE ORDER MARK}, and annotate the new tests. --- src/iniconfig/__init__.py | 6 ------ src/iniconfig/_parse.py | 4 ++++ testing/test_iniconfig.py | 6 +++--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/iniconfig/__init__.py b/src/iniconfig/__init__.py index e9fd907..b84809f 100644 --- a/src/iniconfig/__init__.py +++ b/src/iniconfig/__init__.py @@ -112,9 +112,6 @@ def __init__( if data is None: with open(self.path, encoding=encoding) as fp: data = fp.read() - # Strip a leading UTF-8 BOM so editors that write one still parse. - if data.startswith(""): - data = data.removeprefix("") # Use old behavior (no stripping) for backward compatibility sections_data, sources = _parse.parse_ini_data( @@ -169,9 +166,6 @@ def parse( if data is None: with open(fspath, encoding=encoding) as fp: data = fp.read() - # Strip a leading UTF-8 BOM so editors that write one still parse. - if data.startswith(""): - data = data.removeprefix("") sections_data, sources = _parse.parse_ini_data( fspath, diff --git a/src/iniconfig/_parse.py b/src/iniconfig/_parse.py index 57b9b44..e7a1a8c 100644 --- a/src/iniconfig/_parse.py +++ b/src/iniconfig/_parse.py @@ -1,9 +1,11 @@ from collections.abc import Mapping +from typing import Final from typing import NamedTuple from .exceptions import ParseError COMMENTCHARS = "#;" +UTF8_BOM: Final = "\N{BYTE ORDER MARK}" class ParsedLine(NamedTuple): @@ -34,6 +36,8 @@ def parse_ini_data( - sections_data: mapping of section -> {name -> value} - sources: mapping of (section, name) -> line number """ + data = data.removeprefix(UTF8_BOM) + tokens = parse_lines( path, data.splitlines(True), diff --git a/testing/test_iniconfig.py b/testing/test_iniconfig.py index 488178f..6162256 100644 --- a/testing/test_iniconfig.py +++ b/testing/test_iniconfig.py @@ -414,7 +414,7 @@ def test_unicode_whitespace_in_key_names() -> None: assert config["section"]["key"] == "value" -def test_utf8_bom_file(tmp_path): +def test_utf8_bom_file(tmp_path: Path) -> None: """Files saved with a UTF-8 BOM (common on Windows editors) must parse.""" path = tmp_path / "bom.ini" path.write_bytes(b"\xef\xbb\xbf[section]\nkey = value\n") @@ -422,12 +422,12 @@ def test_utf8_bom_file(tmp_path): assert config["section"]["key"] == "value" -def test_utf8_bom_in_data_string(): +def test_utf8_bom_in_data_string() -> None: config = IniConfig("x.ini", data="\ufeff[section]\nkey = value\n") assert config["section"]["key"] == "value" -def test_parse_utf8_bom_file(tmp_path): +def test_parse_utf8_bom_file(tmp_path: Path) -> None: path = tmp_path / "bom.ini" path.write_bytes(b"\xef\xbb\xbf[section]\nkey = value\n") config = IniConfig.parse(path)