Repository: dynaconf/dynaconf
Pull request #944 (2023-06-29 20:55:10)

Fix - Template substitution with variable update
This fixes the misbehavior of template substitution when the variable to be substituted gets updated by another source, as described in #575 #603 #666 and #690

**Main changes:**
- avoid replacement of `Lazy` objects by it's value inside `parse_conf_data`
- add `_safe_get` and `_safe_items` methods to `DynaBox`.
- small unrelated refactors

## Problem Overview

There were some points where earlier evaluation was called internally. Although those calls were unnecessary, they shouldn't have cause this bug by itself because evaluating a Lazy object means only asking it to return the final value (it doesn't replace itself with the value), so later on it should have used the correct value. Nevertheless, `parse_conf_data` was replacing the Lazy object with its early evaluation value, and that was the main cause of the problem.

## Additional Notes

Related to this, it would be nice to:
- solve previous related problems more cleanly (eg, #910 #933)
- track and remove unnecessary internal recursive evaluation calls (may improve performance).

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *dynaconf* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 7d78da19558fce8c10cdcf427e41711fc08bd937 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/dynaconf/deploys/649defb0e73e5e0008868563 |
pedro-psb: CI failure seems related to `actions/checkout`: https://github.com/actions/checkout/issues/417
I'll try bumping v2 to v3 first. If this doesn't work, I'll explore the other solutions proposed there.

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/dynaconf/dynaconf/pull/944?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf) Report
> Merging [#944](https://app.codecov.io/gh/dynaconf/dynaconf/pull/944?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf) (c48b014) into [master](https://app.codecov.io/gh/dynaconf/dynaconf/commit/f0d966589e5016b8051555a583db5cf3ab555938?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf) (f0d9665) will **decrease** coverage by `0.15%`.
> The diff coverage is `90.90%`.

:exclamation: Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please [install the Github App Integration](https://github.com/apps/codecov) for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf).

```diff
@@            Coverage Diff             @@
##           master     #944      +/-   ##
==========================================
- Coverage   99.79%   99.64%   -0.15%     
==========================================
  Files          22       22              
  Lines        1976     1994      +18     
==========================================
+ Hits         1972     1987      +15     
- Misses          4        7       +3     
```


| [Impacted Files](https://app.codecov.io/gh/dynaconf/dynaconf/pull/944?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf) | Coverage Δ | |
|---|---|---|
| [dynaconf/cli.py](https://app.codecov.io/gh/dynaconf/dynaconf/pull/944?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf#diff-ZHluYWNvbmYvY2xpLnB5) | `99.28% <ø> (ø)` | |
| [dynaconf/loaders/py\_loader.py](https://app.codecov.io/gh/dynaconf/dynaconf/pull/944?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf#diff-ZHluYWNvbmYvbG9hZGVycy9weV9sb2FkZXIucHk=) | `100.00% <ø> (ø)` | |
| [dynaconf/utils/boxing.py](https://app.codecov.io/gh/dynaconf/dynaconf/pull/944?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf#diff-ZHluYWNvbmYvdXRpbHMvYm94aW5nLnB5) | `94.00% <50.00%> (-6.00%)` | :arrow_down: |
| [dynaconf/base.py](https://app.codecov.io/gh/dynaconf/dynaconf/pull/944?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf#diff-ZHluYWNvbmYvYmFzZS5weQ==) | `99.80% <100.00%> (+<0.01%)` | :arrow_up: |
| [dynaconf/utils/\_\_init\_\_.py](https://app.codecov.io/gh/dynaconf/dynaconf/pull/944?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf#diff-ZHluYWNvbmYvdXRpbHMvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |
| [dynaconf/utils/inspect.py](https://app.codecov.io/gh/dynaconf/dynaconf/pull/944?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf#diff-ZHluYWNvbmYvdXRpbHMvaW5zcGVjdC5weQ==) | `100.00% <100.00%> (ø)` | |
| [dynaconf/utils/parse\_conf.py](https://app.codecov.io/gh/dynaconf/dynaconf/pull/944?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf#diff-ZHluYWNvbmYvdXRpbHMvcGFyc2VfY29uZi5weQ==) | `100.00% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf)


Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3a8c1a6..2c883e2 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -216,7 +216,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     services:
       vault:
-        image: vault
+        image: hashicorp/vault:latest
         ports:
           - 8200:8200
         env:
diff --git a/dynaconf/base.py b/dynaconf/base.py
index d746fe2..a1e8ebe 100644
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -33,8 +33,8 @@ from dynaconf.utils.boxing import DynaBox
 from dynaconf.utils.files import find_file
 from dynaconf.utils.functional import empty
 from dynaconf.utils.functional import LazyObject
+from dynaconf.utils.parse_conf import apply_converter
 from dynaconf.utils.parse_conf import converters
-from dynaconf.utils.parse_conf import get_converter
 from dynaconf.utils.parse_conf import Lazy
 from dynaconf.utils.parse_conf import parse_conf_data
 from dynaconf.utils.parse_conf import true_values
@@ -415,7 +415,7 @@ class Settings:
         # If we've reached the end, or parent key not found, then return result
         if not keys or result == default:
             if cast and cast in converters:
-                return get_converter(cast, result, box_settings=self)
+                return apply_converter(cast, result, box_settings=self)
             elif cast is True:
                 return parse_conf_data(result, tomlfy=True, box_settings=self)
             return result
@@ -500,7 +500,7 @@ class Settings:
 
         data = (parent or self.store).get(key, default)
         if cast:
-            data = get_converter(cast, data, box_settings=self)
+            data = apply_converter(cast, data, box_settings=self)
         return data
 
     def exists(self, key, fresh=False):
@@ -539,7 +539,7 @@ class Settings:
         data = self.environ.get(key, default)
         if data:
             if cast in converters:
-                data = get_converter(cast, data, box_settings=self)
+                data = apply_converter(cast, data, box_settings=self)
             elif cast is True:
                 data = parse_conf_data(data, tomlfy=True, box_settings=self)
         return data
@@ -862,8 +862,11 @@ class Settings:
         tree[split_keys[-1]] = value
 
         if existing_data:
+            old_data = DynaBox(
+                {split_keys[0]: existing_data}, box_settings=self
+            )
             new_data = object_merge(
-                old=DynaBox({split_keys[0]: existing_data}),
+                old=old_data,
                 new=new_data,
                 full_path=split_keys,
             )
@@ -985,7 +988,7 @@ class Settings:
                     )
                     loader_identifier = updated_identifier
 
-        if isinstance(value, dict):
+        if isinstance(value, dict) and not isinstance(value, DynaBox):
             value = DynaBox(value, box_settings=self)
 
         self.store[key] = value
diff --git a/dynaconf/cli.py b/dynaconf/cli.py
index 7ff84aa..b8785fe 100644
--- a/dynaconf/cli.py
+++ b/dynaconf/cli.py
@@ -810,11 +810,11 @@ def validate(ctx, path):  # pragma: no cover
 
 
 from dynaconf.utils.inspect import (
-    KeyNotFound,
+    KeyNotFoundError,
     builtin_dumpers,
     inspect_settings,
-    EnvNotFound,
-    InvalidOutputFormat,
+    EnvNotFoundError,
+    OutputFormatError,
 )
 
 INSPECT_FORMATS = list(builtin_dumpers.keys())
@@ -854,7 +854,7 @@ def inspect(ctx, key, env, format, descending):  # pragma: no cover
             ascending_order=(not descending),
         )
         click.echo()
-    except (KeyNotFound, EnvNotFound, InvalidOutputFormat) as err:
+    except (KeyNotFoundError, EnvNotFoundError, OutputFormatError) as err:
         click.echo(err)
         sys.exit(1)
 
diff --git a/dynaconf/loaders/py_loader.py b/dynaconf/loaders/py_loader.py
index 25d8da5..8e12282 100644
--- a/dynaconf/loaders/py_loader.py
+++ b/dynaconf/loaders/py_loader.py
@@ -32,7 +32,6 @@ def load(
     # setup SourceMetadata (for inspecting)
     loader_identifier = SourceMetadata(identifier, mod.__name__, "global")
 
-    # breakpoint()
     load_from_python_object(
         obj, mod, settings_module, key, loader_identifier, validate=validate
     )
diff --git a/dynaconf/utils/__init__.py b/dynaconf/utils/__init__.py
index 6149ef8..08381ee 100644
--- a/dynaconf/utils/__init__.py
+++ b/dynaconf/utils/__init__.py
@@ -70,7 +70,16 @@ def object_merge(
             if correct_case_key:
                 new[correct_case_key] = new.pop(new_key)
 
-        for old_key, value in old.items():
+        def safe_items(data):
+            """
+            Get items from DynaBox without triggering recursive evaluation
+            """
+            if data.__class__.__name__ == "DynaBox":
+                return data._safe_items()
+            else:
+                return data.items()
+
+        for old_key, value in safe_items(old):
 
             # This is for when the dict exists internally
             # but the new value on the end of full path is the same
@@ -412,7 +421,13 @@ def recursively_evaluate_lazy_format(
 
     For example: Evaluate values inside lists and dicts
     """
+    return _recursively_evaluate_lazy_format(value, settings)
+
 
+def _recursively_evaluate_lazy_format(
+    value: Any, settings: Settings | LazySettings
+) -> Any:
+    """Recursive implementation. Separate for easier debugging."""
     if getattr(value, "_dynaconf_lazy_format", None):
         value = value(settings)
 
@@ -420,7 +435,7 @@ def recursively_evaluate_lazy_format(
         # Keep the original type, can be a BoxList
         value = value.__class__(
             [
-                recursively_evaluate_lazy_format(item, settings)
+                _recursively_evaluate_lazy_format(item, settings)
                 for item in value
             ]
         )
diff --git a/dynaconf/utils/boxing.py b/dynaconf/utils/boxing.py
index ff78f12..8276992 100644
--- a/dynaconf/utils/boxing.py
+++ b/dynaconf/utils/boxing.py
@@ -48,6 +48,14 @@ class DynaBox(Box):
             n_item = find_the_correct_casing(item, self) or item
             return super().__getitem__(n_item, *args, **kwargs)
 
+    def _safe_get(self, item, *args, **kwargs):
+        """Get item bypassing recursive evaluation"""
+        try:
+            return super().__getitem__(item, *args, **kwargs)
+        except (AttributeError, KeyError):
+            n_item = find_the_correct_casing(item, self) or item
+            return super().__getitem__(n_item, *args, **kwargs)
+
     def __copy__(self):
         return self.__class__(
             super(Box, self).copy(),
diff --git a/dynaconf/utils/inspect.py b/dynaconf/utils/inspect.py
index c3bf013..000750c 100644
--- a/dynaconf/utils/inspect.py
+++ b/dynaconf/utils/inspect.py
@@ -36,15 +36,15 @@ OutputFormat = Union[Literal["yaml"], Literal["json"], Literal["json-compact"]]
 DumperType = Callable[[dict, TextIO], None]
 
 
-class KeyNotFound(Exception):
+class KeyNotFoundError(Exception):
     pass
 
 
-class EnvNotFound(Exception):
+class EnvNotFoundError(Exception):
     pass
 
 
-class InvalidOutputFormat(Exception):
+class OutputFormatError(Exception):
     pass
 
 
@@ -78,7 +78,7 @@ def inspect_settings(
 
     setting_envs = {_env.env for _env in settings._loaded_by_loaders.keys()}
     if env and env.lower() not in setting_envs:
-        raise EnvNotFound(f"The requested env is not valid: {env!r}")
+        raise EnvNotFoundError(f"The requested env is not valid: {env!r}")
 
     def env_filter(src: SourceMetadata) -> bool:
         return src.env.lower() == env.lower() if env else True
@@ -89,7 +89,7 @@ def inspect_settings(
         filter_src_metadata=env_filter,
     )
     if key_dotted_path and not history:
-        raise KeyNotFound(
+        raise KeyNotFoundError(
             f"The requested key was not found: {key_dotted_path!r}"
         )
 
@@ -126,7 +126,7 @@ def inspect_settings(
     try:
         dumper = builtin_dumpers[output_format.lower()]
     except KeyError:
-        raise InvalidOutputFormat(
+        raise OutputFormatError(
             f"The desired format is not available: {output_format!r}"
         )
 
diff --git a/dynaconf/utils/parse_conf.py b/dynaconf/utils/parse_conf.py
index e5a1afc..35faad8 100644
--- a/dynaconf/utils/parse_conf.py
+++ b/dynaconf/utils/parse_conf.py
@@ -267,7 +267,12 @@ converters = {
 }
 
 
-def get_converter(converter_key, value, box_settings):
+def apply_converter(converter_key, value, box_settings):
+    """
+    Get converter and apply it to @value.
+
+    Lazy converters will return Lazy objects for later evaluation.
+    """
     converter = converters[converter_key]
     try:
         converted_value = converter(value, box_settings=box_settings)
@@ -356,7 +361,7 @@ def _parse_conf_data(data, tomlfy=False, box_settings=None):
 
         # Parse the converters iteratively
         for converter_key in converter_key_list[::-1]:
-            value = get_converter(converter_key, value, box_settings)
+            value = apply_converter(converter_key, value, box_settings)
     else:
         value = parse_with_toml(data) if tomlfy else data
 
@@ -367,6 +372,11 @@ def _parse_conf_data(data, tomlfy=False, box_settings=None):
 
 
 def parse_conf_data(data, tomlfy=False, box_settings=None):
+    """
+    Apply parsing tokens recursively and return transformed data.
+
+    Strings with lazy parser (e.g, @format) will become Lazy objects.
+    """
 
     # fix for https://github.com/dynaconf/dynaconf/issues/595
     if isnamedtupleinstance(data):
@@ -382,7 +392,16 @@ def parse_conf_data(data, tomlfy=False, box_settings=None):
             for item in data
         ]
 
-    if isinstance(data, (dict, DynaBox)):
+    if isinstance(data, DynaBox):
+        # recursively parse inner dict items
+        _parsed = DynaBox({}, box_settings=box_settings)
+        for k, v in data._safe_items():
+            _parsed[str(k)] = parse_conf_data(
+                v, tomlfy=tomlfy, box_settings=box_settings
+            )
+        return _parsed
+
+    if isinstance(data, dict):
         # recursively parse inner dict items
         _parsed = {}
         for k, v in data.items():
diff --git a/dynaconf/vendor/box/box.py b/dynaconf/vendor/box/box.py
index 0b4c1d2..e94aaa6 100644
--- a/dynaconf/vendor/box/box.py
+++ b/dynaconf/vendor/box/box.py
@@ -261,6 +261,10 @@ class Box(dict):
     def items(self):
         return [(x, self[x]) for x in self.keys()]
 
+    def _safe_items(self):
+        """Get items list without triggering recursive evaluation"""
+        return [(x, self._safe_get(x)) for x in self.keys()]
+
     def __get_default(self, item):
         default_value = self._box_config['default_box_attr']
         if default_value in (self.__class__, dict):


Test patch:
diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml
index 6c368a8..c8c0bbf 100644
--- a/tests/docker-compose.yml
+++ b/tests/docker-compose.yml
@@ -5,7 +5,7 @@ services:
         ports:
             - 6379:6379
     vault:
-        image: vault
+        image: hashicorp/vault:latest
         ports:
             - 8200:8200
         environment:
diff --git a/tests/test_inspect.py b/tests/test_inspect.py
index 3526dbe..d5e8537 100644
--- a/tests/test_inspect.py
+++ b/tests/test_inspect.py
@@ -14,11 +14,11 @@ import pytest
 from dynaconf import Dynaconf
 from dynaconf.utils.inspect import _ensure_serializable
 from dynaconf.utils.inspect import _get_data_by_key
-from dynaconf.utils.inspect import EnvNotFound
+from dynaconf.utils.inspect import EnvNotFoundError
 from dynaconf.utils.inspect import get_history
 from dynaconf.utils.inspect import inspect_settings
-from dynaconf.utils.inspect import InvalidOutputFormat
-from dynaconf.utils.inspect import KeyNotFound
+from dynaconf.utils.inspect import KeyNotFoundError
+from dynaconf.utils.inspect import OutputFormatError
 from dynaconf.validator import Validator
 from dynaconf.vendor.ruamel import yaml
 
@@ -761,7 +761,7 @@ def test_caveat__get_history_env_true_workaround(tmp_path):
 
 def test_inspect_exception_key_not_found():
     settings = Dynaconf()
-    with pytest.raises(KeyNotFound):
+    with pytest.raises(KeyNotFoundError):
         inspect_settings(settings, key_dotted_path="non_existant")
 
 
@@ -783,13 +783,13 @@ def test_inspect_empty_settings(capsys):
 
 def test_inspect_exception_env_not_found():
     settings = Dynaconf(environments=True)
-    with pytest.raises(EnvNotFound):
+    with pytest.raises(EnvNotFoundError):
         inspect_settings(settings, env="non_existant")
 
 
 def test_inspect_exception_invalid_format():
     settings = Dynaconf()
-    with pytest.raises(InvalidOutputFormat):
+    with pytest.raises(OutputFormatError):
         inspect_settings(
             settings, output_format="invalid_format"  # type: ignore
         )
diff --git a/tests_functional/issues/575_603_666_690__envvar_with_template_substitution/Makefile b/tests_functional/issues/575_603_666_690__envvar_with_template_substitution/Makefile
new file mode 100644
index 0000000..c16a235
--- /dev/null
+++ b/tests_functional/issues/575_603_666_690__envvar_with_template_substitution/Makefile
@@ -0,0 +1,4 @@
+.PHONY: test
+
+test:
+	pytest -v app_test.py
diff --git a/tests_functional/issues/575_603_666_690__envvar_with_template_substitution/app_test.py b/tests_functional/issues/575_603_666_690__envvar_with_template_substitution/app_test.py
new file mode 100644
index 0000000..a2ad3bb
--- /dev/null
+++ b/tests_functional/issues/575_603_666_690__envvar_with_template_substitution/app_test.py
@@ -0,0 +1,176 @@
+"""
+Given
+- some source is using template substitution '@format {variable}'
+- the template and the variable are inside some nested structure
+When
+- the variable referred to in the template is updated by another source
+Should
+- execute substitution with the updated/newest value
+Instead (before fix)
+- the old value (from original source) is used
+
+https://github.com/dynaconf/dynaconf/issues/575
+https://github.com/dynaconf/dynaconf/issues/603
+https://github.com/dynaconf/dynaconf/issues/690
+"""
+from __future__ import annotations
+
+import os
+from textwrap import dedent
+from unittest import mock
+
+import pytest
+
+from dynaconf import Dynaconf
+
+
+def create_file(filename: str, data: str):
+    """Utility to help create tmp files"""
+    with open(filename, "w") as f:
+        f.write(dedent(data))
+    return filename
+
+
+def test_envvar_override_without_nesting(tmp_path):
+    """Works without nesting structures"""
+    environ = {"DYNACONF_DB_PORT": "9999"}
+    filename = create_file(
+        filename=tmp_path / "s.toml",
+        data="""\
+        DB_PORT='1234'
+        DB_ADDRESS='@format 127.0.0.1:{this.db_port}'
+        """,
+    )
+    with mock.patch.dict(os.environ, environ):
+        settings = Dynaconf(settings_file=filename)
+        assert settings.db_port == 9999  # unwanted casting, but not related
+        assert settings.db_address == "127.0.0.1:9999"
+
+
+def test_envvar_override_with_nesting(tmp_path):
+    environ = {"DYNACONF_DATABASE__DB_PORT": "9999"}
+    filename = create_file(
+        filename=tmp_path / "s.toml",
+        data="""\
+        [database]
+        DB_PORT='1234'
+        DB_ADDRESS='@format 127.0.0.1:{this.database.db_port}'
+        """,
+    )
+    with mock.patch.dict(os.environ, environ):
+        settings = Dynaconf(settings_file=filename)
+        assert settings.database.db_address == "127.0.0.1:9999"
+
+
+def test_update_method_with_nesting(tmp_path):
+    environ = {}
+    filename = create_file(
+        filename=tmp_path / "s.toml",
+        data="""\
+        [database]
+        DB_PORT='1234'
+        DB_ADDRESS='@format 127.0.0.1:{this.database.db_port}'
+        """,
+    )
+    with mock.patch.dict(os.environ, environ):
+        settings = Dynaconf(settings_file=filename)
+        settings.update({"database": {"db_port": "9999"}}, merge=True)
+        assert settings.database.db_port == "9999"
+        assert settings.database.db_address == "127.0.0.1:9999"
+
+
+def test_575_example(tmp_path):
+    environ = {"DYNACONF_INGEST__BASE_PATH": "/tmp/ingest"}
+    filename = create_file(
+        filename=tmp_path / "s.toml",
+        data="""\
+        INSTALL_DIR = "/opt/myapp"
+        LOGS_DIR = "@format {this.INSTALL_DIR}/logs"
+
+        [ingest]
+        BASE_PATH = "/opt/ingest"
+        LOGS_DIR = "@format {this.ingest.BASE_PATH}/logs"
+        """,
+    )
+    with mock.patch.dict(os.environ, environ):
+        settings = Dynaconf(settings_file=filename)
+        assert settings.ingest.LOGS_DIR == "/tmp/ingest/logs"
+        assert settings.ingest.BASE_PATH == "/tmp/ingest"
+
+
+def test_690_example(tmp_path):
+    environ = {"MYAPP_SERVER__VERSION__RELEASE": "7.2"}
+    filename = create_file(
+        filename=tmp_path / "s.yaml",
+        data="""\
+        SERVER:
+          VERSION:
+            RELEASE: "6.10"
+            SNAP: 22
+          DEPLOY_WORKFLOW: "deploy-sat-jenkins"
+          DEPLOY_ARGUMENTS:
+            sat_version: "@format {this.server.version.release}"
+            snap_version: "@format {this.server.version.snap}"
+            rhel_version: '7'
+        """,
+    )
+    with mock.patch.dict(os.environ, environ):
+        settings = Dynaconf(
+            envvar_prefix="MYAPP",
+            core_loaders=["YAML"],
+            settings_file=filename,
+        )
+        assert settings.server.deploy_arguments.sat_version == "7.2"
+        assert settings.server.deploy_arguments.snap_version == "22"
+        assert settings.server.deploy_arguments.rhel_version == "7"
+
+
+def test_666_example(tmp_path):
+    environ = {
+        "MYAPP_SERVER__VERSION__RELEASE": "7.2",
+        "ENV_FOR_DYNACONF": "env1",
+    }
+    filename = create_file(
+        filename=tmp_path / "s.yaml",
+        data="""\
+        default:
+            a_level1:
+                prefix: default
+                level2:
+                    value: "@format {this.a_level1.prefix}-xxx"
+            b_level1:
+                prefix: default
+                level2:
+                    some_key: True
+                    value: "@format {this.b_level1.prefix}-xxx"
+        env1:
+            dynaconf_merge: True
+            a_level1:
+                prefix: env1
+
+            b_level1:
+                prefix: env1
+                level2:
+                    some_key: False
+        """,
+    )
+    with mock.patch.dict(os.environ, environ):
+        settings = Dynaconf(settings_file=filename, environments=True)
+        assert settings.a_level1.level2.value == "env1-xxx"
+        assert settings.b_level1.level2.value == "env1-xxx"
+
+
+def test_603_example(tmp_path):
+    environ = {"DYNACONF_DATABASE__DB_PORT": "9999"}
+    filename = create_file(
+        filename=tmp_path / "s.toml",
+        data="""\
+        [default.database]
+        DB_HOST = "127.0.0.1"
+        DB_PORT = "5432"
+        DB_ADDRESS = "@format {this.database.DB_HOST}:{this.database.DB_PORT}"
+        """,
+    )
+    with mock.patch.dict(os.environ, environ):
+        settings = Dynaconf(settings_file=filename, environments=True)
+        assert settings.database.db_address == "127.0.0.1:9999"
diff --git a/tests_functional/issues/879_cli_validate_isinstance/pyrightconfig.json b/tests_functional/issues/879_cli_validate_isinstance/pyrightconfig.json
deleted file mode 100644
index f8af880..0000000
--- a/tests_functional/issues/879_cli_validate_isinstance/pyrightconfig.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  "venvPath": "/home/pedro-psb/dev/projects/dynaconf-issues/879_cli_validate_isinstace",
-  "venv": "venv"
-}
diff --git a/tests_functional/test_vault.sh b/tests_functional/test_vault.sh
index 8e93bf7..3cd15a5 100755
--- a/tests_functional/test_vault.sh
+++ b/tests_functional/test_vault.sh
@@ -1,5 +1,10 @@
 #!/bin/bash
-docker run --rm --name dynaconf_with_vault -d -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' -p 8200:8200 vault || true
+docker run --rm \
+    --name dynaconf_with_vault -d \
+    -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' \
+    -p 8200:8200 \
+    hashicorp/vault:latest \
+    || true
 sleep 5
 cd tests_functional/vault
 pwd
diff --git a/tests_functional/test_vault_userpass.sh b/tests_functional/test_vault_userpass.sh
index 760c603..e0b6c0f 100755
--- a/tests_functional/test_vault_userpass.sh
+++ b/tests_functional/test_vault_userpass.sh
@@ -1,5 +1,10 @@
 #!/bin/bash
-docker run --rm --name dynaconf_with_vault -d -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' -p 8200:8200 vault || true
+docker run --rm \
+    --name dynaconf_with_vault -d \
+    -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' \
+    -p 8200:8200 \
+    hashicorp/vault:latest \
+    || true
 sleep 5
 cd tests_functional/vault_userpass
 pwd