From 7ae272f28584dbac7ef16e5f0f6080c139ce25a3 Mon Sep 17 00:00:00 2001 From: Mohammed Alkindi Date: Thu, 20 Aug 2026 12:53:37 +0400 Subject: [PATCH 1/2] fix: preserve a leading BOM when set_key and unset_key rewrite a file The parser strips a leading UTF-8 BOM so the first variable is read correctly, but the stripped BOM never reaches the bindings that set_key and unset_key write back out. Rewriting a file that had one therefore dropped it silently, changing a part of the file the caller did not ask to touch. Carry it across in rewrite(), which both writers share. --- src/dotenv/main.py | 8 ++++++++ tests/test_main.py | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 3123690a..4a5b776d 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -172,6 +172,14 @@ def rewrite( try: with source: + # The parser strips a leading BOM, so it never reaches the + # bindings the caller writes out. Carry it across here, or + # rewriting a file that has one silently drops it. + if source.seekable(): + if source.read(1) == "\ufeff": + dest.write("\ufeff") + else: + source.seek(0) yield (source, dest) except BaseException as err: error = err diff --git a/tests/test_main.py b/tests/test_main.py index 6f9d4c5c..dbb191fb 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -106,6 +106,17 @@ def test_set_key_preserves_file_mode(dotenv_path): assert mode_before == mode_after +def test_set_key_preserves_byte_order_mark(dotenv_path): + dotenv_path.write_bytes(b"\xef\xbb\xbfa=x\n") + + dotenv.set_key(dotenv_path, "b", "y") + + contents = dotenv_path.read_bytes() + assert contents.startswith(b"\xef\xbb\xbf") + assert contents.count(b"\xef\xbb\xbf") == 1 + assert dotenv.dotenv_values(dotenv_path) == {"a": "x", "b": "y"} + + def test_rewrite_closes_file_handle_on_lstat_failure(tmp_path): dotenv_path = tmp_path / ".env" dotenv_path.write_text("a=x\n") @@ -303,6 +314,17 @@ def test_unset_encoding(dotenv_path): assert dotenv_path.read_text(encoding=encoding) == "" +def test_unset_preserves_byte_order_mark(dotenv_path): + dotenv_path.write_bytes(b"\xef\xbb\xbfa=x\nb=y\n") + + result = dotenv.unset_key(dotenv_path, "b") + + assert result == (True, "b") + contents = dotenv_path.read_bytes() + assert contents.startswith(b"\xef\xbb\xbf") + assert dotenv.dotenv_values(dotenv_path) == {"a": "x"} + + def test_unset_non_existent_file(tmp_path): nx_path = tmp_path / "nx" logger = logging.getLogger("dotenv.main") From b636f75406b9d6e84821f92b2039b0cb5f7bc78c Mon Sep 17 00:00:00 2001 From: Mohammed Alkindi Date: Thu, 20 Aug 2026 13:08:53 +0400 Subject: [PATCH 2/2] docs: add CHANGELOG entry for #687 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00f08f58..619ccd22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed - An unquoted empty value followed by an inline comment (e.g. `KEY= # comment`) is now parsed as an empty string instead of the comment text by [@Noethix55555] in [#663] +- A leading UTF-8 BOM is now preserved when `set_key` or `unset_key` rewrites a `.env` file, instead of being silently dropped by [@MohammedAlkindi] in [#687] ## [1.2.3] - 2026-08-16 @@ -448,6 +449,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]). [#640]: https://github.com/theskumar/python-dotenv/pull/640 [#663]: https://github.com/theskumar/python-dotenv/pull/663 [#680]: https://github.com/theskumar/python-dotenv/pull/680 +[#687]: https://github.com/theskumar/python-dotenv/pull/687 [790c5c0]: https://github.com/theskumar/python-dotenv/commit/790c5c02991100aa1bf41ee5330aca75edc51311 @@ -487,6 +489,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]). [@lsmith77]: https://github.com/lsmith77 [@matthewfranglen]: https://github.com/matthewfranglen [@mgorny]: https://github.com/mgorny +[@MohammedAlkindi]: https://github.com/MohammedAlkindi [@naorlivne]: https://github.com/naorlivne [@Noethix55555]: https://github.com/Noethix55555 [@qnighy]: https://github.com/qnighy