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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `set_key` and `unset_key` no longer leave a `.tmp_*` file behind on Windows when writing a read-only `.env` fails, and the error raised is the one from the failed write rather than from cleaning up the temporary file by [@MohammedAlkindi] in [#686]
- `load_dotenv`, `dotenv_values`, `get_key`, `set_key`, `unset_key` and the CLI `--file` option now expand a leading `~` to the user's home directory by [@veeceey] in [#615]
- `find_dotenv` and the IPython `%dotenv` magic now expand a leading `~` in the file name by [@theskumar] in [#714]
- `load_dotenv` now finds a current-directory `.env` when called directly as a
`threading.Thread` target, fixing [#531] reported by [@kazzmir]

## [1.2.4] - 2026-10-01

Expand Down Expand Up @@ -443,6 +445,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]).
[#454]: https://github.com/theskumar/python-dotenv/issues/454
[#474]: https://github.com/theskumar/python-dotenv/issues/474
[#523]: https://github.com/theskumar/python-dotenv/issues/523
[#531]: https://github.com/theskumar/python-dotenv/issues/531
[#553]: https://github.com/theskumar/python-dotenv/issues/553
[#569]: https://github.com/theskumar/python-dotenv/issues/569
[#583]: https://github.com/theskumar/python-dotenv/issues/583
Expand Down Expand Up @@ -505,6 +508,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]).
[@jamalkamaladdin]: https://github.com/jamalkamaladdin
[@jankislinger]: https://github.com/jankislinger
[@jctanner]: https://github.com/jctanner
[@kazzmir]: https://github.com/kazzmir
[@kurtmckee]: https://github.com/kurtmckee
[@larsks]: https://github.com/larsks
[@lsmith77]: https://github.com/lsmith77
Expand Down
7 changes: 6 additions & 1 deletion src/dotenv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import stat
import sys
import tempfile
import threading
from collections import OrderedDict
from contextlib import contextmanager
from typing import IO, Dict, Iterable, Iterator, Mapping, Optional, Tuple, Union
Expand Down Expand Up @@ -415,7 +416,11 @@ def _is_debugger():
assert frame.f_back is not None
frame = frame.f_back
frame_filename = frame.f_code.co_filename
path = os.path.dirname(os.path.abspath(frame_filename))
path = (
os.getcwd()
if frame.f_globals is vars(threading)
else os.path.dirname(os.path.abspath(frame_filename))
)

for dirname in _walk_to_root(path):
check_path = os.path.join(dirname, filename)
Expand Down
94 changes: 94 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import subprocess
import sys
import textwrap
import threading
from unittest import mock

import pytest
Expand Down Expand Up @@ -868,6 +869,99 @@ def test_load_dotenv_in_current_dir(tmp_path):
assert result.stdout == "b\n"


@mock.patch.dict(os.environ, {}, clear=True)
def test_load_dotenv_as_thread_target(tmp_path, monkeypatch):
dotenv_path = tmp_path / ".env"
dotenv_path.write_text("a=b")
monkeypatch.chdir(tmp_path)

thread = threading.Thread(target=dotenv.load_dotenv)
thread.start()
thread.join()

assert not thread.is_alive()
assert os.environ.get("a") == "b"


def test_find_dotenv_from_module_named_threading(tmp_path):
project_dir = tmp_path / "project"
project_dir.mkdir()
dotenv_path = project_dir / ".env"
dotenv_path.write_text("a=b")
module_path = project_dir / "threading.py"
module_path.touch()
elsewhere = tmp_path / "elsewhere"
elsewhere.mkdir()
runner_path = elsewhere / "runner.py"
runner_path.write_text(
textwrap.dedent(
f"""
namespace = {{"__name__": "threading"}}
exec(
compile(
"from dotenv import find_dotenv\\nresult = find_dotenv()\\n",
{str(module_path)!r},
"exec",
),
namespace,
)
print(namespace["result"])
"""
)
)

result = subprocess.run(
[sys.executable, str(runner_path)],
cwd=elsewhere,
check=True,
capture_output=True,
text=True,
)

assert result.stdout == f"{dotenv_path}\n"


def test_load_dotenv_from_thread_wrapper_uses_wrapper_location(tmp_path):
project_dir = tmp_path / "project"
project_dir.mkdir()
(project_dir / ".env").write_text("a=b")
wrapper_path = project_dir / "wrapper.py"
wrapper_path.write_text(
"from dotenv import load_dotenv\n\ndef load():\n load_dotenv()\n"
)
elsewhere = tmp_path / "elsewhere"
elsewhere.mkdir()
runner_path = elsewhere / "runner.py"
runner_path.write_text(
textwrap.dedent(
f"""
import os
import sys
import threading

sys.path.insert(0, {str(project_dir)!r})
from wrapper import load

os.environ.pop("a", None)
thread = threading.Thread(target=load)
thread.start()
thread.join()
print(os.environ["a"])
"""
)
)

result = subprocess.run(
[sys.executable, str(runner_path)],
cwd=elsewhere,
check=True,
capture_output=True,
text=True,
)

assert result.stdout == "b\n"


def test_dotenv_values_file(dotenv_path):
dotenv_path.write_text("a=b")

Expand Down
Loading