Skip to content

Commit 8e44f25

Browse files
committed
Add argument to interpolate variables from a user provided dict instead of os.environ
1 parent 36516a7 commit 8e44f25

File tree

4 files changed

+48
-4
lines changed

4 files changed

+48
-4
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this
66
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- Add support to use a custom dict instead of os.environ for variable
13+
interpolating when calling `dotenv_values` (by [@johnbergvall])
14+
815
## [0.19.0] - 2021-07-24
916

1017
### Changed

README.md

+21
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,27 @@ config = {
9898
}
9999
```
100100

101+
Further advanced use by overriding os.environ with a user defined dict:
102+
103+
```python
104+
import os
105+
import subprocess
106+
from dotenv import dotenv_values
107+
108+
deploy_env = {
109+
'FALLBACK_DOMAIN': 'example.org',
110+
'VERSION': '1.5',
111+
}
112+
env = dotenv_values('.env.deployment01', base_env={
113+
**dotenv_values('.env.base', base_env=deploy_env),
114+
**deploy_env,
115+
})
116+
subprocess.call(
117+
['/usr/bin/docker', 'stack', 'deploy', '-c', 'docker-compose.yml', 'myproject'],
118+
env={**deploy_env, **env},
119+
)
120+
```
121+
101122
### Parse configuration as a stream
102123

103124
`load_dotenv` and `dotenv_values` accept [streams][python_streams] via their `stream`

src/dotenv/main.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(
3939
encoding: Union[None, str] = None,
4040
interpolate: bool = True,
4141
override: bool = True,
42+
base_env: Mapping[str, Optional[str]] = os.environ
4243
) -> None:
4344
self.dotenv_path = dotenv_path # type: Optional[Union[str, _PathLike]]
4445
self.stream = stream # type: Optional[IO[str]]
@@ -47,6 +48,7 @@ def __init__(
4748
self.encoding = encoding # type: Union[None, str]
4849
self.interpolate = interpolate # type: bool
4950
self.override = override # type: bool
51+
self.base_env = base_env # type: Mapping[str, Optional[str]]
5052

5153
@contextmanager
5254
def _get_stream(self) -> Iterator[IO[str]]:
@@ -71,7 +73,9 @@ def dict(self) -> Dict[str, Optional[str]]:
7173
raw_values = self.parse()
7274

7375
if self.interpolate:
74-
self._dict = OrderedDict(resolve_variables(raw_values, override=self.override))
76+
self._dict = OrderedDict(
77+
resolve_variables(raw_values, override=self.override, base_env=self.base_env)
78+
)
7579
else:
7680
self._dict = OrderedDict(raw_values)
7781

@@ -212,6 +216,7 @@ def unset_key(
212216
def resolve_variables(
213217
values: Iterable[Tuple[str, Optional[str]]],
214218
override: bool,
219+
base_env: Mapping[str, Optional[str]] = os.environ,
215220
) -> Mapping[str, Optional[str]]:
216221
new_values = {} # type: Dict[str, Optional[str]]
217222

@@ -222,11 +227,11 @@ def resolve_variables(
222227
atoms = parse_variables(value)
223228
env = {} # type: Dict[str, Optional[str]]
224229
if override:
225-
env.update(os.environ) # type: ignore
230+
env.update(base_env) # type: ignore
226231
env.update(new_values)
227232
else:
228233
env.update(new_values)
229-
env.update(os.environ) # type: ignore
234+
env.update(base_env) # type: ignore
230235
result = "".join(atom.resolve(env) for atom in atoms)
231236

232237
new_values[name] = result
@@ -334,6 +339,7 @@ def dotenv_values(
334339
verbose: bool = False,
335340
interpolate: bool = True,
336341
encoding: Optional[str] = "utf-8",
342+
base_env: Mapping[str, Optional[str]] = os.environ,
337343
) -> Dict[str, Optional[str]]:
338344
"""
339345
Parse a .env file and return its content as a dict.
@@ -342,8 +348,8 @@ def dotenv_values(
342348
- *stream*: `StringIO` object with .env content, used if `dotenv_path` is `None`.
343349
- *verbose*: whether to output a warning the .env file is missing. Defaults to
344350
`False`.
345-
in `.env` file. Defaults to `False`.
346351
- *encoding*: encoding to be used to read the file.
352+
- *base_env*: dict with initial environment. Defaults to os.environ
347353
348354
If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file.
349355
"""
@@ -357,4 +363,5 @@ def dotenv_values(
357363
interpolate=interpolate,
358364
override=True,
359365
encoding=encoding,
366+
base_env=base_env,
360367
).dict()

tests/test_main.py

+9
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,15 @@ def test_dotenv_values_file(dotenv_file):
325325
assert result == {"a": "b"}
326326

327327

328+
def test_dotenv_values_file_base_env(dotenv_file):
329+
with open(dotenv_file, "w") as f:
330+
f.write("a=${var}")
331+
332+
result = dotenv.dotenv_values(dotenv_file, base_env={'var': 'b'})
333+
334+
assert result == {"a": "b"}
335+
336+
328337
@pytest.mark.parametrize(
329338
"env,string,interpolate,expected",
330339
[

0 commit comments

Comments
 (0)