Skip to content

Commit 423c1cd

Browse files
committed
unlink_after_load
1 parent f288da1 commit 423c1cd

File tree

2 files changed

+198
-1
lines changed

2 files changed

+198
-1
lines changed

src/dotenv/main.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ def load_dotenv(
341341
override: bool = False,
342342
interpolate: bool = True,
343343
encoding: Optional[str] = "utf-8",
344+
unlink_after_load: bool = False,
344345
) -> bool:
345346
"""Parse a .env file and then load all the variables found as environment variables.
346347
@@ -352,6 +353,8 @@ def load_dotenv(
352353
override: Whether to override the system environment variables with the variables
353354
from the `.env` file.
354355
encoding: Encoding to be used to read the file.
356+
unlink_after_load: Whether to remove the .env file after successfully loading it.
357+
Only works when dotenv_path is provided (not with stream). Defaults to False.
355358
Returns:
356359
Bool: True if at least one environment variable is set else False
357360
@@ -380,7 +383,17 @@ def load_dotenv(
380383
override=override,
381384
encoding=encoding,
382385
)
383-
return dotenv.set_as_environment_variables()
386+
result = dotenv.set_as_environment_variables()
387+
388+
# Unlink the file after loading if requested and file exists
389+
if unlink_after_load and dotenv_path and os.path.isfile(dotenv_path):
390+
try:
391+
os.unlink(dotenv_path)
392+
logger.debug("Removed dotenv file: %s", dotenv_path)
393+
except OSError as e:
394+
logger.debug("Failed to remove dotenv file %s: %s", dotenv_path, e)
395+
396+
return result
384397

385398

386399
def dotenv_values(

tests/test_main.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,3 +520,187 @@ def test_dotenv_values_file_stream(dotenv_path):
520520
result = dotenv.dotenv_values(stream=f)
521521

522522
assert result == {"a": "b"}
523+
524+
525+
class TestLoadDotenvUnlinkAfterLoad:
526+
"""Test cases for the unlink_after_load parameter in load_dotenv."""
527+
528+
def test_unlink_after_load_true_removes_file(self, tmp_path):
529+
"""Test that file is removed when unlink_after_load=True."""
530+
dotenv_file = tmp_path / ".env"
531+
dotenv_file.write_text("TEST_VAR=test_value\n")
532+
533+
# Ensure file exists before loading
534+
assert dotenv_file.exists()
535+
536+
# Load dotenv with unlink_after_load=True
537+
result = dotenv.load_dotenv(dotenv_path=str(dotenv_file), unlink_after_load=True)
538+
539+
# Verify loading was successful
540+
assert result is True
541+
assert os.environ.get("TEST_VAR") == "test_value"
542+
543+
# Verify file was removed
544+
assert not dotenv_file.exists()
545+
546+
# Clean up environment
547+
if "TEST_VAR" in os.environ:
548+
del os.environ["TEST_VAR"]
549+
550+
def test_unlink_after_load_false_keeps_file(self, tmp_path):
551+
"""Test that file is kept when unlink_after_load=False (default)."""
552+
dotenv_file = tmp_path / ".env"
553+
dotenv_file.write_text("TEST_VAR2=test_value2\n")
554+
555+
# Ensure file exists before loading
556+
assert dotenv_file.exists()
557+
558+
# Load dotenv with unlink_after_load=False (default)
559+
result = dotenv.load_dotenv(dotenv_path=str(dotenv_file), unlink_after_load=False)
560+
561+
# Verify loading was successful
562+
assert result is True
563+
assert os.environ.get("TEST_VAR2") == "test_value2"
564+
565+
# Verify file still exists
566+
assert dotenv_file.exists()
567+
568+
# Clean up environment
569+
if "TEST_VAR2" in os.environ:
570+
del os.environ["TEST_VAR2"]
571+
572+
def test_unlink_after_load_default_keeps_file(self, tmp_path):
573+
"""Test that file is kept when unlink_after_load is not specified (default behavior)."""
574+
dotenv_file = tmp_path / ".env"
575+
dotenv_file.write_text("TEST_VAR3=test_value3\n")
576+
577+
# Ensure file exists before loading
578+
assert dotenv_file.exists()
579+
580+
# Load dotenv without specifying unlink_after_load
581+
result = dotenv.load_dotenv(dotenv_path=str(dotenv_file))
582+
583+
# Verify loading was successful
584+
assert result is True
585+
assert os.environ.get("TEST_VAR3") == "test_value3"
586+
587+
# Verify file still exists (default behavior)
588+
assert dotenv_file.exists()
589+
590+
# Clean up environment
591+
if "TEST_VAR3" in os.environ:
592+
del os.environ["TEST_VAR3"]
593+
594+
def test_unlink_after_load_with_nonexistent_file(self, tmp_path):
595+
"""Test that no error occurs when trying to unlink a non-existent file."""
596+
nonexistent_file = tmp_path / "nonexistent.env"
597+
598+
# Ensure file doesn't exist
599+
assert not nonexistent_file.exists()
600+
601+
# Load dotenv with unlink_after_load=True on non-existent file
602+
result = dotenv.load_dotenv(dotenv_path=str(nonexistent_file), unlink_after_load=True)
603+
604+
# Verify loading returns False (no variables loaded)
605+
assert result is False
606+
607+
# Verify no exception was raised and file still doesn't exist
608+
assert not nonexistent_file.exists()
609+
610+
def test_unlink_after_load_with_stream_ignores_unlink(self, tmp_path):
611+
"""Test that unlink_after_load is ignored when using stream instead of file path."""
612+
dotenv_file = tmp_path / ".env"
613+
dotenv_file.write_text("TEST_VAR4=test_value4\n")
614+
615+
# Load using stream with unlink_after_load=True
616+
with open(dotenv_file, 'r') as f:
617+
result = dotenv.load_dotenv(stream=f, unlink_after_load=True)
618+
619+
# Verify loading was successful
620+
assert result is True
621+
assert os.environ.get("TEST_VAR4") == "test_value4"
622+
623+
# Verify file still exists (unlink should be ignored with stream)
624+
assert dotenv_file.exists()
625+
626+
# Clean up environment
627+
if "TEST_VAR4" in os.environ:
628+
del os.environ["TEST_VAR4"]
629+
630+
def test_unlink_after_load_with_empty_file(self, tmp_path):
631+
"""Test unlink behavior with empty dotenv file."""
632+
dotenv_file = tmp_path / ".env"
633+
dotenv_file.write_text("")
634+
635+
# Ensure file exists before loading
636+
assert dotenv_file.exists()
637+
638+
# Load empty dotenv with unlink_after_load=True
639+
result = dotenv.load_dotenv(dotenv_path=str(dotenv_file), unlink_after_load=True)
640+
641+
# Verify loading returns False (no variables loaded)
642+
assert result is False
643+
644+
# Verify file was still removed even though no variables were loaded
645+
assert not dotenv_file.exists()
646+
647+
def test_unlink_after_load_with_verbose_logging(self, tmp_path, caplog):
648+
"""Test that verbose logging shows unlink operation."""
649+
dotenv_file = tmp_path / ".env"
650+
dotenv_file.write_text("TEST_VAR5=test_value5\n")
651+
652+
with caplog.at_level(logging.INFO):
653+
result = dotenv.load_dotenv(
654+
dotenv_path=str(dotenv_file),
655+
unlink_after_load=True,
656+
verbose=True
657+
)
658+
659+
# Verify loading was successful
660+
assert result is True
661+
assert os.environ.get("TEST_VAR5") == "test_value5"
662+
663+
# Verify file was removed
664+
assert not dotenv_file.exists()
665+
666+
# Verify log message about removal
667+
assert any("Removed dotenv file" in record.message for record in caplog.records)
668+
669+
# Clean up environment
670+
if "TEST_VAR5" in os.environ:
671+
del os.environ["TEST_VAR5"]
672+
673+
def test_unlink_after_load_permission_error(self, tmp_path, caplog, monkeypatch):
674+
"""Test handling of permission errors when unlinking."""
675+
dotenv_file = tmp_path / ".env"
676+
dotenv_file.write_text("TEST_VAR6=test_value6\n")
677+
678+
# Mock os.unlink to raise a permission error
679+
original_unlink = os.unlink
680+
def mock_unlink(path):
681+
if str(dotenv_file) in str(path):
682+
raise PermissionError("Permission denied")
683+
return original_unlink(path)
684+
685+
monkeypatch.setattr(os, "unlink", mock_unlink)
686+
687+
with caplog.at_level(logging.WARNING):
688+
result = dotenv.load_dotenv(
689+
dotenv_path=str(dotenv_file),
690+
unlink_after_load=True,
691+
verbose=True
692+
)
693+
694+
# Verify loading was successful despite unlink failure
695+
assert result is True
696+
assert os.environ.get("TEST_VAR6") == "test_value6"
697+
698+
# Verify file still exists due to permission error
699+
assert dotenv_file.exists()
700+
701+
# Verify warning log message about failed removal
702+
assert any("Failed to remove dotenv file" in record.message for record in caplog.records)
703+
704+
# Clean up environment
705+
if "TEST_VAR6" in os.environ:
706+
del os.environ["TEST_VAR6"]

0 commit comments

Comments
 (0)