Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ load_dotenv() # reads variables from a .env file and sets them in os.environ

# Code of your application, which uses environment variables (e.g. from `os.environ` or
# `os.getenv`) as if they came from the actual environment.

# In tests, unload_dotenv() removes those values from os.environ again:
# from dotenv import unload_dotenv
# unload_dotenv()
```

By default, `load_dotenv()` will:
Expand Down
3 changes: 2 additions & 1 deletion src/dotenv/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Optional

from .main import dotenv_values, find_dotenv, get_key, load_dotenv, set_key, unset_key
from .main import dotenv_values, find_dotenv, get_key, load_dotenv, set_key, unload_dotenv, unset_key


def load_ipython_extension(ipython: Any) -> None:
Expand Down Expand Up @@ -42,6 +42,7 @@ def get_cli_string(
__all__ = [
"get_cli_string",
"load_dotenv",
"unload_dotenv",
"dotenv_values",
"get_key",
"set_key",
Expand Down
29 changes: 29 additions & 0 deletions src/dotenv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,35 @@ def load_dotenv(
return dotenv.set_as_environment_variables()


def unload_dotenv(
dotenv_path: Optional[StrPath] = None,
stream: Optional[IO[str]] = None,
encoding: Optional[str] = "utf-8",
) -> bool:
"""Remove variables defined in a `.env` file from `os.environ`.

Useful in tests so `load_dotenv()` does not leak into later cases.

A key is removed only when its current environment value still matches the
value from the file, so an explicit later assignment is left alone.

Returns True if at least one variable was removed.
"""
values = dotenv_values(
dotenv_path=dotenv_path,
stream=stream,
encoding=encoding,
)
Comment on lines +452 to +456
removed = False
for key, value in values.items():
if value is None:
continue
if os.environ.get(key) == value:
del os.environ[key]
Comment on lines +461 to +462
removed = True
return removed


def dotenv_values(
dotenv_path: Optional[StrPath] = None,
stream: Optional[IO[str]] = None,
Expand Down
30 changes: 30 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,36 @@ def test_load_dotenv_existing_file(dotenv_path):
assert os.environ == {"a": "b"}


@mock.patch.dict(os.environ, {}, clear=True)
def test_unload_dotenv_removes_loaded_values(dotenv_path):
dotenv_path.write_text("a=b")
dotenv.load_dotenv(dotenv_path)

result = dotenv.unload_dotenv(dotenv_path)

assert result is True
assert "a" not in os.environ


@mock.patch.dict(os.environ, {}, clear=True)
def test_unload_dotenv_keeps_changed_values(dotenv_path):
dotenv_path.write_text("a=b")
dotenv.load_dotenv(dotenv_path)
os.environ["a"] = "later"

result = dotenv.unload_dotenv(dotenv_path)

assert result is False
assert os.environ["a"] == "later"


@mock.patch.dict(os.environ, {}, clear=True)
def test_unload_dotenv_missing_file(tmp_path):
result = dotenv.unload_dotenv(tmp_path / "missing.env")

assert result is False


@pytest.mark.parametrize(
"flag_value",
[
Expand Down
Loading