Skip to content

Commit 134ed43

Browse files
committed
Allow any text stream (IO[str]) as stream
This applies to the `load_dotenv` and `dotenv_values` functions. This makes it possible to pass a file stream such as `open("foo", "r")` to these functions.
1 parent 955e2a4 commit 134ed43

File tree

3 files changed

+57
-20
lines changed

3 files changed

+57
-20
lines changed

CHANGELOG.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
99

10-
### Added
11-
12-
- The `dotenv_path` argument of `set_key` and `unset_key` now has a type of `Union[str,
13-
os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]).
14-
1510
### Changed
1611

1712
- Require Python 3.5 or a later version. Python 2 and 3.4 are no longer supported. (#341
1813
by [@bbc2]).
1914

15+
### Added
16+
17+
- The `dotenv_path` argument of `set_key` and `unset_key` now has a type of `Union[str,
18+
os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]).
19+
- The `stream` argument of `load_dotenv` and `dotenv_values` can now be a text stream
20+
(`IO[str]`), which includes values like `io.StringIO("foo")` and `open("file.env",
21+
"r")` (#348 by [@bbc2]).
22+
2023
## [0.18.0] - 2021-06-20
2124

2225
### Changed

src/dotenv/main.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@ def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding
3333
class DotEnv():
3434
def __init__(
3535
self,
36-
dotenv_path: Union[str, _PathLike, io.StringIO],
36+
dotenv_path: Optional[Union[str, _PathLike]],
37+
stream: Optional[IO[str]] = None,
3738
verbose: bool = False,
3839
encoding: Union[None, str] = None,
3940
interpolate: bool = True,
4041
override: bool = True,
4142
) -> None:
42-
self.dotenv_path = dotenv_path # type: Union[str,_PathLike, io.StringIO]
43+
self.dotenv_path = dotenv_path # type: Optional[Union[str, _PathLike]]
44+
self.stream = stream # type: Optional[IO[str]]
4345
self._dict = None # type: Optional[Dict[str, Optional[str]]]
4446
self.verbose = verbose # type: bool
4547
self.encoding = encoding # type: Union[None, str]
@@ -48,14 +50,17 @@ def __init__(
4850

4951
@contextmanager
5052
def _get_stream(self) -> Iterator[IO[str]]:
51-
if isinstance(self.dotenv_path, io.StringIO):
52-
yield self.dotenv_path
53-
elif os.path.isfile(self.dotenv_path):
53+
if self.dotenv_path and os.path.isfile(self.dotenv_path):
5454
with io.open(self.dotenv_path, encoding=self.encoding) as stream:
5555
yield stream
56+
elif self.stream is not None:
57+
yield self.stream
5658
else:
5759
if self.verbose:
58-
logger.info("Python-dotenv could not find configuration file %s.", self.dotenv_path or '.env')
60+
logger.info(
61+
"Python-dotenv could not find configuration file %s.",
62+
self.dotenv_path or '.env',
63+
)
5964
yield io.StringIO('')
6065

6166
def dict(self) -> Dict[str, Optional[str]]:
@@ -290,7 +295,7 @@ def _is_interactive():
290295

291296
def load_dotenv(
292297
dotenv_path: Union[str, _PathLike, None] = None,
293-
stream: Optional[io.StringIO] = None,
298+
stream: Optional[IO[str]] = None,
294299
verbose: bool = False,
295300
override: bool = False,
296301
interpolate: bool = True,
@@ -299,7 +304,8 @@ def load_dotenv(
299304
"""Parse a .env file and then load all the variables found as environment variables.
300305
301306
- *dotenv_path*: absolute or relative path to .env file.
302-
- *stream*: `StringIO` object with .env content, used if `dotenv_path` is `None`.
307+
- *stream*: Text stream (such as `io.StringIO`) with .env content, used if
308+
`dotenv_path` is `None`.
303309
- *verbose*: whether to output a warning the .env file is missing. Defaults to
304310
`False`.
305311
- *override*: whether to override the system environment variables with the variables
@@ -308,9 +314,12 @@ def load_dotenv(
308314
309315
If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file.
310316
"""
311-
f = dotenv_path or stream or find_dotenv()
317+
if dotenv_path is None and stream is None:
318+
dotenv_path = find_dotenv()
319+
312320
dotenv = DotEnv(
313-
f,
321+
dotenv_path=dotenv_path,
322+
stream=stream,
314323
verbose=verbose,
315324
interpolate=interpolate,
316325
override=override,
@@ -321,7 +330,7 @@ def load_dotenv(
321330

322331
def dotenv_values(
323332
dotenv_path: Union[str, _PathLike, None] = None,
324-
stream: Optional[io.StringIO] = None,
333+
stream: Optional[IO[str]] = None,
325334
verbose: bool = False,
326335
interpolate: bool = True,
327336
encoding: Optional[str] = "utf-8",
@@ -338,9 +347,12 @@ def dotenv_values(
338347
339348
If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file.
340349
"""
341-
f = dotenv_path or stream or find_dotenv()
350+
if dotenv_path is None and stream is None:
351+
dotenv_path = find_dotenv()
352+
342353
return DotEnv(
343-
f,
354+
dotenv_path=dotenv_path,
355+
stream=stream,
344356
verbose=verbose,
345357
interpolate=interpolate,
346358
override=True,

tests/test_main.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ def test_load_dotenv_redefine_var_used_in_file_with_override(dotenv_file):
277277

278278

279279
@mock.patch.dict(os.environ, {}, clear=True)
280-
def test_load_dotenv_utf_8():
280+
def test_load_dotenv_string_io_utf_8():
281281
stream = io.StringIO("a=à")
282282

283283
result = dotenv.load_dotenv(stream=stream)
@@ -286,6 +286,18 @@ def test_load_dotenv_utf_8():
286286
assert os.environ == {"a": "à"}
287287

288288

289+
@mock.patch.dict(os.environ, {}, clear=True)
290+
def test_load_dotenv_file_stream(dotenv_file):
291+
with open(dotenv_file, "w") as f:
292+
f.write("a=b")
293+
294+
with open(dotenv_file, "r") as f:
295+
result = dotenv.load_dotenv(stream=f)
296+
297+
assert result is True
298+
assert os.environ == {"a": "b"}
299+
300+
289301
def test_load_dotenv_in_current_dir(tmp_path):
290302
dotenv_path = tmp_path / '.env'
291303
dotenv_path.write_bytes(b'a=b')
@@ -353,11 +365,21 @@ def test_dotenv_values_file(dotenv_file):
353365
({}, "a=b\nc=${a}\nd=e\nc=${d}", True, {"a": "b", "c": "e", "d": "e"}),
354366
],
355367
)
356-
def test_dotenv_values_stream(env, string, interpolate, expected):
368+
def test_dotenv_values_string_io(env, string, interpolate, expected):
357369
with mock.patch.dict(os.environ, env, clear=True):
358370
stream = io.StringIO(string)
359371
stream.seek(0)
360372

361373
result = dotenv.dotenv_values(stream=stream, interpolate=interpolate)
362374

363375
assert result == expected
376+
377+
378+
def test_dotenv_values_file_stream(dotenv_file):
379+
with open(dotenv_file, "w") as f:
380+
f.write("a=b")
381+
382+
with open(dotenv_file, "r") as f:
383+
result = dotenv.dotenv_values(stream=f)
384+
385+
assert result == {"a": "b"}

0 commit comments

Comments
 (0)