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"