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 @@ -9,6 +9,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixed

- `load_dotenv` now finds a current-directory `.env` when called directly as a
`threading.Thread` target, fixing [#531] reported by [@kazzmir]
- 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]

## [1.2.3] - 2026-08-16
Expand Down Expand Up @@ -431,6 +433,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 @@ -483,6 +486,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]).
[@jadutter]: https://github.com/jadutter
[@jankislinger]: https://github.com/jankislinger
[@jctanner]: https://github.com/jctanner
[@kazzmir]: https://github.com/kazzmir
[@larsks]: https://github.com/larsks
[@lsmith77]: https://github.com/lsmith77
[@matthewfranglen]: https://github.com/matthewfranglen
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 @@ -372,7 +373,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 @@ -5,6 +5,7 @@
import subprocess
import sys
import textwrap
import threading
from unittest import mock

import pytest
Expand Down Expand Up @@ -676,6 +677,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