diff --git a/CHANGELOG.md b/CHANGELOG.md index d1305894..b022ac46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Add support to use a custom dict instead of os.environ for variable + interpolating when calling `dotenv_values` (by [@johnbergvall]) +- Add override-flag to `dotenv_values` to allow for more advanced + chaining of env-files (#73 #186 by [@johnbergvall]) + ## [0.19.0] - 2021-07-24 ### Changed diff --git a/README.md b/README.md index 9b56b546..88bbb0e9 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,28 @@ config = { } ``` +Further advanced use by overriding os.environ with a user defined dict: + +```python +import os +import subprocess +from dotenv import dotenv_values + +deploy_env = { + 'FALLBACK_DOMAIN': 'example.org', + 'VERSION': '1.5', +} +env = dotenv_values('.env.deployment01', base_env={ + # override=False to ignore local file overrides in interpolations: + **dotenv_values('.env.base', override=False, base_env=deploy_env), + **deploy_env, +}) +subprocess.call( + ['/usr/bin/docker', 'stack', 'deploy', '-c', 'docker-compose.yml', 'myproject'], + env={**deploy_env, **env}, +) +``` + ### Parse configuration as a stream `load_dotenv` and `dotenv_values` accept [streams][python_streams] via their `stream` diff --git a/src/dotenv/main.py b/src/dotenv/main.py index b8d0a4e0..08b6ae2b 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -39,6 +39,7 @@ def __init__( encoding: Union[None, str] = None, interpolate: bool = True, override: bool = True, + base_env: Mapping[str, Optional[str]] = os.environ ) -> None: self.dotenv_path = dotenv_path # type: Optional[Union[str, _PathLike]] self.stream = stream # type: Optional[IO[str]] @@ -47,6 +48,7 @@ def __init__( self.encoding = encoding # type: Union[None, str] self.interpolate = interpolate # type: bool self.override = override # type: bool + self.base_env = base_env # type: Mapping[str, Optional[str]] @contextmanager def _get_stream(self) -> Iterator[IO[str]]: @@ -71,7 +73,9 @@ def dict(self) -> Dict[str, Optional[str]]: raw_values = self.parse() if self.interpolate: - self._dict = OrderedDict(resolve_variables(raw_values, override=self.override)) + self._dict = OrderedDict( + resolve_variables(raw_values, override=self.override, base_env=self.base_env) + ) else: self._dict = OrderedDict(raw_values) @@ -212,6 +216,7 @@ def unset_key( def resolve_variables( values: Iterable[Tuple[str, Optional[str]]], override: bool, + base_env: Mapping[str, Optional[str]] = os.environ, ) -> Mapping[str, Optional[str]]: new_values = {} # type: Dict[str, Optional[str]] @@ -222,11 +227,11 @@ def resolve_variables( atoms = parse_variables(value) env = {} # type: Dict[str, Optional[str]] if override: - env.update(os.environ) # type: ignore + env.update(base_env) # type: ignore env.update(new_values) else: env.update(new_values) - env.update(os.environ) # type: ignore + env.update(base_env) # type: ignore result = "".join(atom.resolve(env) for atom in atoms) new_values[name] = result @@ -332,8 +337,10 @@ def dotenv_values( dotenv_path: Union[str, _PathLike, None] = None, stream: Optional[IO[str]] = None, verbose: bool = False, + override: bool = True, interpolate: bool = True, encoding: Optional[str] = "utf-8", + base_env: Mapping[str, Optional[str]] = os.environ, ) -> Dict[str, Optional[str]]: """ Parse a .env file and return its content as a dict. @@ -342,8 +349,10 @@ def dotenv_values( - *stream*: `StringIO` object with .env content, used if `dotenv_path` is `None`. - *verbose*: whether to output a warning the .env file is missing. Defaults to `False`. - in `.env` file. Defaults to `False`. + - *override*: whether to override the system environment/`base_env` variables with + the variables in `.env` file. Defaults to `True` as opposed to `load_dotenv`. - *encoding*: encoding to be used to read the file. + - *base_env*: dict with initial environment. Defaults to os.environ If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file. """ @@ -355,6 +364,7 @@ def dotenv_values( stream=stream, verbose=verbose, interpolate=interpolate, - override=True, + override=override, encoding=encoding, + base_env=base_env, ).dict() diff --git a/tests/test_main.py b/tests/test_main.py index 13e2791c..0c534925 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -325,6 +325,15 @@ def test_dotenv_values_file(dotenv_file): assert result == {"a": "b"} +def test_dotenv_values_file_base_env(dotenv_file): + with open(dotenv_file, "w") as f: + f.write("a=${var}") + + result = dotenv.dotenv_values(dotenv_file, base_env={'var': 'b'}) + + assert result == {"a": "b"} + + @pytest.mark.parametrize( "env,string,interpolate,expected", [