diff --git a/CHANGELOG.md b/CHANGELOG.md index e5f25414..dc09191e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ 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`, `dotenv_values` and `get_key` now warn when the given `.env` path is a directory instead of silently loading nothing by [@boriskozak] in [#427] ## [1.2.4] - 2026-10-01 @@ -467,6 +468,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]). [#700]: https://github.com/theskumar/python-dotenv/pull/700 [#711]: https://github.com/theskumar/python-dotenv/pull/711 [#714]: https://github.com/theskumar/python-dotenv/pull/714 +[#427]: https://github.com/theskumar/python-dotenv/issues/427 [790c5c0]: https://github.com/theskumar/python-dotenv/commit/790c5c02991100aa1bf41ee5330aca75edc51311 @@ -532,6 +534,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]). [@zueve]: https://github.com/zueve [@JYOuyang]: https://github.com/JYOuyang [@burnout-projects]: https://github.com/burnout-projects +[@boriskozak]: https://github.com/boriskozak [@cpackham-atlnz]: https://github.com/cpackham-atlnz [Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.2.4...HEAD [1.2.4]: https://github.com/theskumar/python-dotenv/compare/v1.2.3...v1.2.4 diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 5faa7f0e..91391ea1 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -69,7 +69,13 @@ def _get_stream(self) -> Iterator[IO[str]]: elif self.stream is not None: yield self.stream else: - if self.verbose: + if self.dotenv_path and os.path.exists(self.dotenv_path): + logger.warning( + "python-dotenv could not load configuration file %s: " + "it is not a file.", + self.dotenv_path, + ) + elif self.verbose: logger.info( "python-dotenv could not find configuration file %s.", self.dotenv_path or ".env", diff --git a/tests/test_main.py b/tests/test_main.py index 930ab171..26287200 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -764,6 +764,53 @@ def test_load_dotenv_no_file_verbose(): ) +@pytest.mark.parametrize("verbose", [False, True]) +@mock.patch.dict(os.environ, {}, clear=True) +def test_load_dotenv_directory_warns(tmp_path, verbose): + logger = logging.getLogger("dotenv.main") + + with ( + mock.patch.object(logger, "warning") as mock_warning, + mock.patch.object(logger, "info") as mock_info, + ): + result = dotenv.load_dotenv(tmp_path, verbose=verbose) + + assert result is False + assert os.environ == {} + mock_warning.assert_called_once_with( + "python-dotenv could not load configuration file %s: it is not a file.", + tmp_path, + ) + mock_info.assert_not_called() + + +def test_dotenv_values_directory_warns(tmp_path): + logger = logging.getLogger("dotenv.main") + + with mock.patch.object(logger, "warning") as mock_warning: + result = dotenv.dotenv_values(tmp_path) + + assert result == {} + mock_warning.assert_called_once_with( + "python-dotenv could not load configuration file %s: it is not a file.", + tmp_path, + ) + + +@mock.patch.dict(os.environ, {}, clear=True) +def test_load_dotenv_directory_with_stream_loads(tmp_path): + logger = logging.getLogger("dotenv.main") + + with mock.patch.object(logger, "warning") as mock_warning: + result = dotenv.load_dotenv( + tmp_path, stream=io.StringIO("FOO=bar\n"), verbose=True + ) + + assert result is True + assert os.environ["FOO"] == "bar" + mock_warning.assert_not_called() + + @pytest.mark.skipif( sys.platform == "win32", reason="This test assumes case-sensitive variable names" )