Repository: dynaconf/dynaconf

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.





Discussion:
https://pyparsing-docs.readthedocs.io/en/latest/HowToUsePyparsing.html can be used to solve the tokenization
Would `pyparsing` be vendored?
@pedro-psb I am not sure yet if we really need pyparsing or if we can solve it by using a simple split() and maybe regexp.

If we need pyparsing, then we must avoid vendoring it and make this feature optional, as an extra dependency to install, not added by default.

```python
THING = "@get VAR @int"
# ImportError, pyparsing is not installed
# pip install pyparsing
```

but probably we can solve with simple split and regex as we have few tokens to extract.

```
@get {variable_name}
@get {variable_name} {@cast}
@get {variable_name} {@cast} {default value}
@get {variable_name} {default value}
```

there are 4 valid states and we can match them using a regexp over the structure. (as long as the default value doesnt start with @

Resolving patch:
diff --git a/dynaconf/base.py b/dynaconf/base.py
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -432,6 +432,12 @@ def _dotted_get(
         :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
--- 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 @@ def evaluate(settings, *args, **kwargs):
     "@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
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -12,6 +12,7 @@
 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