Repository: dynaconf/dynaconf (python)

Issue:
[RFC] add `@get` converter
I need to add a new converter called `@get` to be used to simply call `settings.get` with specified key and use whatever value is there with the current type.

Example:

```python
from dynaconf import Dynaconf
settings = Dynaconf(settings_files=["a.py", "b.py"])

# a.py
THING = "value"

# b.py
ANOTHER_THING = "@get THING"
```

This is different than using `this` inside a lazy `@format/@jinja` because those 2 would cast to string by default or to specified combo like `@int @format {this[THING]}`.

The `@get` would just attempt to perform a `settings.get` and use whatever value is there, it will also allow a default to be specified.

```python
THING = "@get THING @int 789"
```

The above will translate to 

```python
settings.get("THING", default="789", cast="@int")
```

Not sure if this must be lazily evaluated.


This feature will allow the fix of https://github.com/pulp/pulp_ansible/pull/1130 

## Implementation


On https://github.com/dynaconf/dynaconf/blob/6322c4dc4afe0c91160cbdfb069119ca815a60a1/dynaconf/utils/parse_conf.py#L242-L278 


```python
"@get": lambda value: Lazy(value, formatter=getter_formatter)
```

then implement the `getter_formatter` as a function that takes `value` as `"THING @int 789"` parses it and resolves to `settings.get()` call.




Pull request description:
feat: Add `@get` converter to alias existing keys
Fix #1038

https://deploy-preview-1040--dynaconf.netlify.app/dynamic/#name-aliasing 

## Name aliasing

In certain cases you need to reuse some variable value respecting the exact 
data type it is previously defined in another step of the settings load.

Example:

`config.py`
```py
from dynaconf import Dynaconf
settings = Dynaconf(settings_files=["settings.toml", "other.toml"])  
```

`settings.toml`
```toml 
thing = "value"
number = 42
```

`other.toml`
```toml 
other_thing = "@get thing"

# providing default if thing is undefined
other_thing = "@get thing 'default value'"

## type casting
# Ensure the aliased value is of type int
other_number = "@get number @int"

# Ensure type and provide default if number is undefined
other_number = "@get number @int 12"
```

The `@get` is lazily evaluated and subject to `DynaconfFormatError` in case of
malformed expression, it is recommended to add validators.


Patch:
diff --git a/docs/dynamic.md b/docs/dynamic.md
index 2b7b984..10b2d1c 100644
--- a/docs/dynamic.md
+++ b/docs/dynamic.md
@@ -129,4 +129,39 @@ MODEL_SETTINGS_DICT: "@json @jinja {{ this|attr(this.MODEL_TYPE) }}"
 
 # This returns the raw json string
 MODEL_SETTINGS_STR: "@jinja {{ this|attr(this.MODEL_TYPE) }}"
-```
\ No newline at end of file
+```
+
+
+## Name aliasing
+
+In certain cases you need to reuse some variable value respecting the exact 
+data type it is previously defined in another step of the settings load.
+
+Example:
+
+```py title="config.py"
+from dynaconf import Dynaconf
+settings = Dynaconf(settings_files=["settings.toml", "other.toml"])  
+```
+
+```toml title="settings.toml"
+thing = "value"
+number = 42
+```
+
+```toml title="other.toml"
+other_thing = "@get thing"
+
+# providing default if thing is undefined
+other_thing = "@get thing 'default value'"
+
+## type casting
+# Ensure the aliased value is of type int
+other_number = "@get number @int"
+
+# Ensure type and provide default if number is undefined
+other_number = "@get number @int 12"
+```
+
+The `@get` is lazily evaluated and subject to `DynaconfFormatError` in case of
+malformed expression, it is recommended to add validators.
diff --git a/dynaconf/base.py b/dynaconf/base.py
index 7c4674a..505367c 100644
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -432,6 +432,12 @@ class Settings:
         :param default: In case of not found it will be returned
         :param parent: Is there a pre-loaded parent in a nested data?
         """
+        # if parent is not traverseable raise error
+        if parent and not hasattr(parent, "get"):
+            raise AttributeError(
+                f"cannot lookup {dotted_key!r} from {type(parent).__name__!r}"
+            )
+
         split_key = dotted_key.split(".")
         name, keys = split_key[0], split_key[1:]
         result = self.get(name, default=default, parent=parent, **kwargs)
diff --git a/dynaconf/utils/parse_conf.py b/dynaconf/utils/parse_conf.py
index 882110e..657f1c0 100644
--- a/dynaconf/utils/parse_conf.py
+++ b/dynaconf/utils/parse_conf.py
@@ -168,11 +168,46 @@ def _jinja_formatter(value, **context):
     return jinja_env.from_string(value).render(**context)
 
 
+def _get_formatter(value, **context):
+    """
+    Invokes settings.get from the annotation in value.
+
+    value can be one of the following:
+
+    @get KEY
+    @get KEY @int
+    @get KEY default_value
+    @get KEY @int default_value
+
+    @marker KEY_TO_LOOKUP @OPTIONAL_CAST OPTIONAL_DEFAULT_VALUE
+
+    key group will match the key
+    cast group will match anything provided after @
+    the default group will match anything between single or double quotes
+    """
+    pattern = re.compile(
+        r"(?P<key>\w+(?:\.\w+)?)\s*"
+        r"(?:(?P<cast>@\w+)\s*)?"
+        r'(?P<quote>["\']?)'
+        r'\s*(?P<default>[^"\']*)\s*(?P=quote)?'
+    )
+    if match := pattern.match(value.strip()):
+        data = match.groupdict()
+        return context["this"].get(
+            key=data["key"],
+            default=data["default"],
+            cast=data["cast"],
+        )
+    else:
+        raise DynaconfFormatError(f"Error parsing {value} malformed syntax.")
+
+
 class Formatters:
     """Dynaconf builtin formatters"""
 
     python_formatter = BaseFormatter(str.format, "format")
     jinja_formatter = BaseFormatter(_jinja_formatter, "jinja")
+    get_formatter = BaseFormatter(_get_formatter, "get")
 
 
 class Lazy:
@@ -268,6 +303,7 @@ converters = {
     "@merge_unique": lambda value, box_settings: Merge(
         value, box_settings, unique=True
     ),
+    "@get": lambda value: Lazy(value, formatter=Formatters.get_formatter),
     # Special markers to be used as placeholders e.g: in prefilled forms
     # will always return None when evaluated
     "@note": lambda value: None,


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 59da18b..0d51013 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -12,6 +12,7 @@ import pytest
 from dynaconf import add_converter
 from dynaconf import default_settings
 from dynaconf import Dynaconf
+from dynaconf import DynaconfFormatError
 from dynaconf.loaders.json_loader import DynaconfEncoder
 from dynaconf.utils import build_env_list
 from dynaconf.utils import ensure_a_list
@@ -519,3 +520,21 @@ def test_boolean_fix():
     assert boolean_fix("TrueNotOnly") == "TrueNotOnly"
     assert boolean_fix("FalseNotOnly") == "FalseNotOnly"
     assert boolean_fix("NotOnlyFalse") == "NotOnlyFalse"
+
+
+def test_get_converter(settings):
+    """Ensure the work of @get converter"""
+    settings.set("FOO", 12)
+    settings.set("BAR", "@get FOO")
+    assert settings.BAR == settings.FOO == 12
+
+    settings.set("ZAZ", "@get RAZ @float 42")
+    assert settings.ZAZ == 42.0
+
+
+def test_get_converter_error_when_converting(settings):
+    """Malformed declaration errors"""
+    settings.set("BLA", "@get")
+
+    with pytest.raises(DynaconfFormatError):
+        settings.BLA