Repository: dynaconf/dynaconf (python)

Issue:
[bug] Regression: get_history() of dynamic value throwing DynaconfFormatError
**Describe the bug**
In release 3.2.6 calling `get_history()` on a `LazySettings` object with a dotted key having a dynamic `@format` value incorrectly throws a `dynaconf.utils.parse_conf.DynaconfFormatError`. This is a regression from release 3.2.5. A minimal reproducable example is provided below.

**To Reproduce**
Steps to reproduce the behavior:

1. Having the following folder structure

<!-- Describe or use the command `$ tree -v` and paste below -->

<details>
<summary> Project structure </summary>

```bash
.
├── get_history_bug.py
└── settings.toml

1 directory, 2 files
```
</details>

2. Having the following config files:

<!-- Please adjust if you are using different files and formats! -->

<details>
<summary> Config files </summary>

**settings.toml**
```toml
[default]

[a]
b = "foo"
c = "bar"
d = "@format {this.a.b} {this.a.c}"
```

</details>

3. Having the following app code:

<details>
<summary> Code </summary>

**get_history_bug.py**
```python
from dynaconf import Dynaconf, get_history
from pprint import pprint

settings = Dynaconf(settings_files=["settings.toml"])

def main() -> None:
    pprint(settings.as_dict())
    print(f"Expecting to get the history of key 'a.d' with value {settings['a']['d']}")
    pprint(get_history(settings, "a.d"))

if __name__ == "__main__":
    main()
```

</details>

4. Executing under the following environment

<details>
<summary> Execution </summary>

```bash
% python3 -m venv .venv
% source .venv/bin/activate
% pip3 install dynaconf==3.2.6
Collecting dynaconf==3.2.6
  Obtaining dependency information for dynaconf==3.2.6 from https://files.pythonhosted.org/packages/e2/14/c8a7d861262139688fa465d2e27ff7113764d6fa03b15b9c7b666729ea2e/dynaconf-3.2.6-py2.py3-none-any.whl.metadata
  Using cached dynaconf-3.2.6-py2.py3-none-any.whl.metadata (9.1 kB)
Using cached dynaconf-3.2.6-py2.py3-none-any.whl (231 kB)
Installing collected packages: dynaconf
Successfully installed dynaconf-3.2.6

[notice] A new release of pip is available: 23.2.1 -> 24.2
[notice] To update, run: pip install --upgrade pip
% python3 get_history_bug.py
{'A': {'b': 'foo', 'c': 'bar', 'd': 'foo bar'}, 'DEFAULT': {}}
Expecting to get the history of key 'a.d' with value foo bar
Traceback (most recent call last):
  File "/home/glenn.mcallister/dynaconf-mre/.venv/lib64/python3.12/site-packages/dynaconf/utils/parse_conf.py", line 151, in __call__
    return self.function(value, **context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'dict' object has no attribute 'a'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/glenn.mcallister/dynaconf-mre/get_history_bug.py", line 12, in <module>
    main()
  File "/home/glenn.mcallister/dynaconf-mre/get_history_bug.py", line 9, in main
    pprint(get_history(settings, "a.d"))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/glenn.mcallister/dynaconf-mre/.venv/lib64/python3.12/site-packages/dynaconf/utils/inspect.py", line 264, in get_history
    data = _get_data_by_key(data, key, sep=sep) if key else data
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/glenn.mcallister/dynaconf-mre/.venv/lib64/python3.12/site-packages/dynaconf/utils/inspect.py", line 335, in _get_data_by_key
    return traverse_data(data, key_dotted_path)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/glenn.mcallister/dynaconf-mre/.venv/lib64/python3.12/site-packages/dynaconf/utils/inspect.py", line 331, in traverse_data
    result = result[key]
             ~~~~~~^^^^^
  File "/home/glenn.mcallister/dynaconf-mre/.venv/lib64/python3.12/site-packages/dynaconf/utils/boxing.py", line 26, in evaluate
    return recursively_evaluate_lazy_format(value, settings)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/glenn.mcallister/dynaconf-mre/.venv/lib64/python3.12/site-packages/dynaconf/utils/__init__.py", line 424, in recursively_evaluate_lazy_format
    return _recursively_evaluate_lazy_format(value, settings)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/glenn.mcallister/dynaconf-mre/.venv/lib64/python3.12/site-packages/dynaconf/utils/__init__.py", line 432, in _recursively_evaluate_lazy_format
    value = value(settings)
            ^^^^^^^^^^^^^^^
  File "/home/glenn.mcallister/dynaconf-mre/.venv/lib64/python3.12/site-packages/dynaconf/utils/parse_conf.py", line 234, in __call__
    result = self.formatter(self.value, **self.context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/glenn.mcallister/dynaconf-mre/.venv/lib64/python3.12/site-packages/dynaconf/utils/parse_conf.py", line 155, in __call__
    raise DynaconfFormatError(
dynaconf.utils.parse_conf.DynaconfFormatError: Dynaconf can't interpolate variable because 'dict' object has no attribute 'a'
```

</details>

**Expected behavior**
As in releases 3.2.4 and 3.2.5 I expected to get the history for the key, as demonstrated below with 3.2.5:

<details>
<summary> Execution </summary>

```bash
% python3 -m venv .venv
% source .venv/bin/activate
% pip3 install dynaconf==3.2.5
Collecting dynaconf==3.2.5
  Obtaining dependency information for dynaconf==3.2.5 from https://files.pythonhosted.org/packages/d6/b1/9b995a140fa4984047b44653036b655ed52680a2293a481614539bc01a07/dynaconf-3.2.5-py2.py3-none-any.whl.metadata
  Using cached dynaconf-3.2.5-py2.py3-none-any.whl.metadata (9.1 kB)
Using cached dynaconf-3.2.5-py2.py3-none-any.whl (230 kB)
Installing collected packages: dynaconf
Successfully installed dynaconf-3.2.5

[notice] A new release of pip is available: 23.2.1 -> 24.2
[notice] To update, run: pip install --upgrade pip
% python3 get_history_bug.py
{'A': {'b': 'foo', 'c': 'bar', 'd': 'foo bar'}, 'DEFAULT': {}}
Expecting to get the history of key 'a.d' with value foo bar
[{'env': 'default',
  'identifier': '/home/glenn.mcallister/dynaconf-mre/settings.toml',
  'loader': 'toml',
  'merged': False,
  'value': '@format {this.a.b} {this.a.c}'}]
```

</details>

**Environment (please complete the following information):**
 - OS: Linux/Fedora39
 - Dynaconf Version: 3.2.6
 - Frameworks in use: None

**Additional context**
- N/A

Pull request:
Fix get_history behavior regarding lazy values
get_history() should provide the original representation, so the user can reliably inspect its data.
The evaluted value might be hard to track and potentially confusing to inspect.

Closes #1180

> [!NOTE]
> This should be backported to the 3.2.6 stream branch so it can be released before 3.3.0

Patch:
diff --git a/docs/flask.md b/docs/flask.md
index 49886df..c138562 100644
--- a/docs/flask.md
+++ b/docs/flask.md
@@ -23,7 +23,7 @@ FlaskDynaconf(app)
 
 Dynaconf transform nested data structures to a new DynaBox instance, this is an
 object that allows dot notation access such as `app.config.key.value.other`.
-However this feature is imcompatible with some Flask extensions, for example:
+However this feature is incompatible with some Flask extensions, for example:
 `Flask-Alembic`.
 
 So in case you see a `BoxKeyError` when running your app with you can run it on
diff --git a/dynaconf/utils/inspect.py b/dynaconf/utils/inspect.py
index eee06f0..fdf77f7 100644
--- a/dynaconf/utils/inspect.py
+++ b/dynaconf/utils/inspect.py
@@ -17,6 +17,7 @@ from typing import Union
 from dynaconf.loaders.base import SourceMetadata
 from dynaconf.utils.boxing import DynaBox
 from dynaconf.utils.functional import empty
+from dynaconf.utils.parse_conf import Lazy
 from dynaconf.vendor.box.box_list import BoxList
 from dynaconf.vendor.ruamel.yaml import YAML
 
@@ -322,13 +323,19 @@ def _get_data_by_key(
     if sep in key_dotted_path:
         key_dotted_path = key_dotted_path.replace(sep, ".")
 
+    def handle_repr(value):
+        # lazy values shouldnt be evaluated for inspecting
+        if isinstance(value, Lazy):
+            return value._dynaconf_encode()
+        return value
+
     def traverse_data(data, path):
         # transform `a.b.c` in successive calls to `data['a']['b']['c']`
         path = path.split(".")
         root_key, nested_keys = path[0], path[1:]
-        result = data[root_key]
+        result = data._safe_get(root_key)
         for key in nested_keys:
-            result = result[key]
+            result = handle_repr(result._safe_get(key))
         return result
 
     try:


Test patch:
diff --git a/tests/test_inspect.py b/tests/test_inspect.py
index 1119edf..2e275c3 100644
--- a/tests/test_inspect.py
+++ b/tests/test_inspect.py
@@ -559,6 +559,37 @@ def test_get_history_env_and_key_filter(tmp_path):
     assert history[1]["value"] == "from_prod_b"
 
 
+def tests_get_history_with_variable_interpolation():
+    """Variable interpolation is not evaluated.
+    https://github.com/dynaconf/dynaconf/issues/1180
+
+    The original issue was about an exception being raised when there was
+    variable interpolation involved. But in the end, the get_history shouldnt
+    evaluate the interpolations:
+
+    - History should accurately inform the order and content of what the user loaded.
+    - For a key with a interpolation value, the accurate representation is not the evaluated
+      value, but the original template string.
+    - The evaluated depends on other keys, so for
+      history inspecting it is more reliable to show what variables were used so the user
+      can verify if everything happened as expected.
+    """
+    data = {
+        "a": {
+            "b": "foo",
+            "c": "bar",
+            "d": "@format {this.a.b} {this.a.c}",
+        }
+    }
+
+    settings = Dynaconf(**data)
+
+    assert settings.a.d == "foo bar"
+    history = get_history(settings, "a.d")
+    # shows the not-evaluated (raw) value
+    assert history[0]["value"] == "@format {this.a.b} {this.a.c}"
+
+
 def test_caveat__get_history_env_true(tmp_path):
     """
     Given environments=True and sources=file