Skip to content

Commit 3b1e86b

Browse files
committed
Add function to write header block to .env file
1 parent 01f8997 commit 3b1e86b

File tree

3 files changed

+87
-2
lines changed

3 files changed

+87
-2
lines changed

src/dotenv/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any, Optional
22

3-
from .main import (dotenv_values, find_dotenv, get_key, load_dotenv, set_key,
4-
unset_key)
3+
from .main import (dotenv_values, find_dotenv, get_key, load_dotenv, set_header,
4+
set_key, unset_key)
55

66

77
def load_ipython_extension(ipython: Any) -> None:
@@ -42,6 +42,7 @@ def get_cli_string(
4242
__all__ = ['get_cli_string',
4343
'load_dotenv',
4444
'dotenv_values',
45+
'set_header',
4546
'get_key',
4647
'set_key',
4748
'unset_key',

src/dotenv/main.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,36 @@ def dotenv_values(
396396
override=True,
397397
encoding=encoding,
398398
).dict()
399+
400+
401+
def set_header(
402+
dotenv_path: StrPath,
403+
header: str,
404+
encoding: Optional[str] = "utf-8",
405+
) -> Tuple[bool, Optional[str]]:
406+
"""
407+
Adds or Updates a header in the .env file
408+
409+
Parameters:
410+
dotenv_path: Absolute or relative path to .env file.
411+
header: The desired header block
412+
encoding: Encoding to be used to read the file.
413+
Returns:
414+
Bool: True if at least one environment variable is set else False
415+
Str: The header that was written
416+
"""
417+
with rewrite(dotenv_path, encoding=encoding) as (source, dest):
418+
if not header or not header.strip():
419+
logger.info("Ignoring empty header.")
420+
return False, header
421+
422+
header = header.strip()
423+
lines = header.split("\n")
424+
for i, line in enumerate(lines):
425+
if not line.startswith("# "):
426+
lines[i] = f"# {line}"
427+
header = "\n".join(lines)
428+
text = "".join(atom for atom in source.readlines() if not atom.startswith("#"))
429+
dest.write(f"{header}\n{text}\n")
430+
431+
return True, header

tests/test_main.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,54 @@ def test_dotenv_values_file_stream(dotenv_path):
396396
result = dotenv.dotenv_values(stream=f)
397397

398398
assert result == {"a": "b"}
399+
400+
401+
@pytest.mark.parametrize(
402+
"header",
403+
[
404+
"",
405+
" ",
406+
None,
407+
],
408+
)
409+
def test_set_header_empty(dotenv_path, header):
410+
logger = logging.getLogger("dotenv.main")
411+
412+
with mock.patch.object(logger, "info") as mock_info:
413+
result, *_ = dotenv.set_header(dotenv_path, header)
414+
assert not result
415+
416+
mock_info.assert_called()
417+
418+
419+
@pytest.mark.parametrize(
420+
"new_header, expected, expected_header, old_header, content",
421+
[
422+
("# header only", True, "# header only", "", ""),
423+
("# single-line header", True, "# single-line header", "", "a=b\nc=d"),
424+
("# multi-line\n# header", True, "# multi-line\n# header", "", "a=b\nc=d"),
425+
(
426+
"Single-line, no comment header",
427+
True,
428+
"# Single-line, no comment header",
429+
"",
430+
"a=b",
431+
),
432+
("# New header", True, "# New header", "# Old header", "a=b\nc=d"),
433+
],
434+
)
435+
def test_set_header(
436+
dotenv_path, new_header, expected, expected_header, old_header, content
437+
):
438+
logger = logging.getLogger("dotenv.main")
439+
dotenv_path.write_text(f"{old_header}\n{content}")
440+
441+
with mock.patch.object(logger, "warning") as mock_warning:
442+
result, written = dotenv.set_header(dotenv_path, new_header)
443+
assert result == expected
444+
assert written == expected_header
445+
446+
text = dotenv_path.read_text()
447+
assert content in text
448+
assert expected_header in text
449+
mock_warning.assert_not_called()

0 commit comments

Comments
 (0)