Repository: dynaconf/dynaconf

Issue:
RFC: Template Substitutions - dynaconf-render-template plugin
Require: #146 

## Allow template substitutions

There are 2 types of variables that would be useful to have access within dynaconf settings

- Environment Variables `${HOME}`, `${PWD}`
- Dynaconf variables `settings.get('value')`, `settings.ROOT_PATH_FOR_DYNACONF`

When setting a new variable it would be nice to have access to that vars like using special marker `$D{}` or `@D{}` (to be defined).

To avoid overhead, this feature must be enabled via plugin.

```python
pip install dynaconf-template-render
```

```yaml
default:
  databases:
    default:
      name: foo_database
      driver: sqlite3
      file: $D{ROOT_PATH_FOR_DYNACONF}/foo.db
   conf_location: $D{HOME}/.config/foo/  
```

Then after executing the loaders (the bottom of `.execute_loaders` method) there will be a `plugin_hook` which will be called having `obj` on context, then plugin can make changes to `obj` (in place)


Discussion:
External Tracker: https://pulp.plan.io/issues/4679#note-3

Resolving patch:
diff --git a/dynaconf/base.py b/dynaconf/base.py
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -25,6 +25,7 @@
 from dynaconf.utils.functional import empty
 from dynaconf.utils.functional import LazyObject
 from dynaconf.utils.parse_conf import converters
+from dynaconf.utils.parse_conf import evaluate_lazy_format
 from dynaconf.utils.parse_conf import parse_conf_data
 from dynaconf.utils.parse_conf import true_values
 from dynaconf.validator import ValidatorList
@@ -90,6 +91,7 @@ def __init__(self, **kwargs):
         self._kwargs = kwargs
         super(LazySettings, self).__init__()
 
+    @evaluate_lazy_format
     def __getattr__(self, name):
         """Allow getting keys from self.store using dot notation"""
         if self._wrapped is empty:
@@ -276,6 +278,7 @@ def _dotted_get(self, dotted_key, default=None, parent=None, **kwargs):
             ".".join(keys), default=default, parent=result, **kwargs
         )
 
+    @evaluate_lazy_format
     def get(
         self,
         key,
diff --git a/dynaconf/utils/parse_conf.py b/dynaconf/utils/parse_conf.py
--- a/dynaconf/utils/parse_conf.py
+++ b/dynaconf/utils/parse_conf.py
@@ -1,11 +1,21 @@
 import json
 import os
+from functools import wraps
 
 import toml
 from box import BoxKeyError
 
 from dynaconf.utils.boxing import DynaBox
 
+try:
+    from jinja2 import Environment
+
+    jinja_env = Environment()
+    for p_method in ("abspath", "realpath", "relpath", "dirname", "basename"):
+        jinja_env.filters[p_method] = getattr(os.path, p_method)
+except ImportError:  # pragma: no cover
+    jinja_env = None
+
 true_values = ("t", "true", "enabled", "1", "on", "yes", "True")
 false_values = ("f", "false", "disabled", "0", "off", "no", "False", "")
 
@@ -42,13 +52,55 @@ class Merge(MetaValue):
     dynaconf_merge = True
 
 
+class LazyFormat:
+    """Holds data to format lazily."""
+
+    dynaconf_lazy_format = True
+
+    def __init__(self, value, formatter=str.format):
+        self.value = value
+        self.formatter = formatter
+
+    @property
+    def context(self):
+        """Builds a context for formatting."""
+        return {"env": os.environ, "this": self.settings}
+
+    def __call__(self, settings):
+        """LazyValue triggers format lazily."""
+        self.settings = settings
+        return self.formatter(self.value, **self.context)
+
+
+def evaluate_lazy_format(f):
+    """Marks a method on Settings instance to
+    lazily evaluate LazyFormat objects upon access."""
+
+    @wraps(f)
+    def evaluate(settings, *args, **kwargs):
+        value = f(settings, *args, **kwargs)
+        if getattr(value, "dynaconf_lazy_format", None):
+            return value(settings)
+        return value
+
+    return evaluate
+
+
+def jinja_formatter(value, **context):
+    if jinja_env is None:  # pragma: no cover
+        raise ImportError(
+            "jinja2 must be installed to enable '@jinja' settings in dynaconf"
+        )
+    return jinja_env.from_string(value).render(**context)
+
+
 converters = {
     "@int": int,
     "@float": float,
-    "@bool": (
-        lambda value: True if str(value).lower() in true_values else False
-    ),
+    "@bool": lambda value: str(value).lower() in true_values,
     "@json": json.loads,
+    "@format": lambda value: LazyFormat(value),
+    "@jinja": lambda value: LazyFormat(value, formatter=jinja_formatter),
     # Meta Values to trigger pre assignment actions
     "@reset": lambda value: Reset(value),
     "@del": lambda value: Del(value),
diff --git a/example/format/app.py b/example/format/app.py
new file mode 100644
--- /dev/null
+++ b/example/format/app.py
@@ -0,0 +1,18 @@
+import os
+
+from dynaconf import settings
+
+assert settings.get("USERNAME") is None
+assert settings.DATABASE_NAME == "my_database.db"
+assert os.environ["USERNAME"] == "robert_plant"
+
+DB_PATH = "/home/robert_plant/databases/my_database.db"
+assert settings.DATABASE_PATH == DB_PATH, settings.DATABASE_PATH
+print(settings.DATABASE_PATH)
+
+
+DB_PATH_JINJA = "/home/robert_plant/development/my_database.db"
+assert (
+    settings.DATABASE_PATH_JINJA == DB_PATH_JINJA
+), settings.DATABASE_PATH_JINJA
+print(settings.DATABASE_PATH_JINJA)


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -12,6 +12,9 @@
 from dynaconf.utils import upperfy
 from dynaconf.utils.files import find_file
 from dynaconf.utils.files import get_local_filename
+from dynaconf.utils.parse_conf import evaluate_lazy_format
+from dynaconf.utils.parse_conf import jinja_formatter
+from dynaconf.utils.parse_conf import LazyFormat
 from dynaconf.utils.parse_conf import parse_conf_data
 from dynaconf.utils.parse_conf import unparse_conf_data
 
@@ -242,3 +245,59 @@ def test_upperfy():
     )
     assert upperfy("foo_bar") == "FOO_BAR"
     assert upperfy("foo_BAR") == "FOO_BAR"
+
+
+def test_lazy_format_class():
+    value = LazyFormat("{this[FOO]}/bar")
+    settings = {"FOO": "foo"}
+    assert value(settings) == "foo/bar"
+
+
+def test_evaluate_lazy_format_decorator():
+    class Settings:
+        FOO = "foo"
+
+        @evaluate_lazy_format
+        def get(self):
+            return parse_conf_data("@format {this.FOO}/bar")
+
+    settings = Settings()
+    assert settings.get() == "foo/bar"
+
+
+def test_lazy_format_on_settings(settings):
+    os.environ["ENV_THING"] = "LazyFormat"
+    settings.set("set_1", "really")
+    settings.set("lazy", "@format {env[ENV_THING]}/{this[set_1]}/{this.SET_2}")
+    settings.set("set_2", "works")
+
+    assert settings.LAZY == settings.get("lazy") == "LazyFormat/really/works"
+
+
+def test_lazy_format_class_jinja():
+    value = LazyFormat("{{this['FOO']}}/bar", formatter=jinja_formatter)
+    settings = {"FOO": "foo"}
+    assert value(settings) == "foo/bar"
+
+
+def test_evaluate_lazy_format_decorator_jinja():
+    class Settings:
+        FOO = "foo"
+
+        @evaluate_lazy_format
+        def get(self):
+            return parse_conf_data("@jinja {{this.FOO}}/bar")
+
+    settings = Settings()
+    assert settings.get() == "foo/bar"
+
+
+def test_lazy_format_on_settings_jinja(settings):
+    os.environ["ENV_THING"] = "LazyFormat"
+    settings.set("set_1", "really")
+    settings.set(
+        "lazy", "@jinja {{env.ENV_THING}}/{{this['set_1']}}/{{this.SET_2}}"
+    )
+    settings.set("set_2", "works")
+
+    assert settings.LAZY == settings.get("lazy") == "LazyFormat/really/works"