Repository: dynaconf/dynaconf

Issue:
[bug] Attribute error when accessing formatted value in layered config 
**Describe the bug**
Attribute error when accessing settings value in layered configuration with multiple settings files.

<details>
<summary>Exception</summary>

```
Traceback (most recent call last):
  File "app.py", line 11, in <module>
    assert settings['s3_url'] == expected_value # fails
  File "/home/user/dev/venv/lib/python3.7/site-packages/dynaconf/utils/functional.py", line 17, in inner
    return func(self._wrapped, *args)
  File "/home/user/dev/venv/lib/python3.7/site-packages/dynaconf/base.py", line 285, in __getitem__
    value = self.get(item, default=empty)
  File "/home/user/dev/venv/lib/python3.7/site-packages/dynaconf/base.py", line 419, in get
    data = (parent or self.store).get(key, default)
  File "/home/user/dev/venv/lib/python3.7/site-packages/dynaconf/utils/boxing.py", line 15, in evaluate
    value = f(dynabox, item, *args, **kwargs)
  File "/home/user/dev/venv/lib/python3.7/site-packages/dynaconf/utils/boxing.py", line 64, in get
    return super(DynaBox, self).get(item, default, *args, **kwargs)
  File "/home/user/dev/venv/lib/python3.7/site-packages/dynaconf/vendor/box/box.py", line 109, in get
    return B[C]
  File "/home/user/dev/venv/lib/python3.7/site-packages/dynaconf/utils/boxing.py", line 23, in evaluate
    return recursively_evaluate_lazy_format(value, settings)
  File "/home/user/dev/venv/lib/python3.7/site-packages/dynaconf/utils/__init__.py", line 355, in recursively_evaluate_lazy_format
    value = value(settings)
  File "/home/user/dev/venv/lib/python3.7/site-packages/dynaconf/utils/parse_conf.py", line 176, in __call__
    return self.formatter(self.value, **self.context)
  File "/home/user/dev/venv/lib/python3.7/site-packages/dynaconf/utils/parse_conf.py", line 138, in __call__
    return self.function(value, **context)
AttributeError: 'Settings' object has no attribute 's3_protocol'
```

</details>

**To Reproduce**
Steps to reproduce the behavior:

1. Having the following folder structure

<details>
<summary> Project structure </summary>

```bash
$ ls
.  ..  app.py  conf-test-default.toml  conf-test-layer.toml
```
</details>

2. Having the following config files:

<details>
<summary> Config files </summary>

**conf-test-default.toml**
```toml
s3_protocol = 's3a'
s3_url = '@format {this.s3_protocol}://{this.s3_bucket}'
```
and

**conf-test-layer.toml**
```toml
s3_bucket = 'kewl_bucket'
```

</details>

3. Having the following app code:

<details>
<summary> Code </summary>

**app.py**
```python
from dynaconf import Dynaconf

settings_files = ['conf-test-default.toml', 'conf-test-layer.toml']

settings = Dynaconf(settings_files=settings_files)

expected_value = 's3a://kewl_bucket'

assert settings.s3_url == expected_value # succeeds

assert settings['s3_url'] == expected_value # fails

assert settings.get('s3_url', default='s3://default') == expected_values # fails

assert settings('s3_url', cast=str) == expected_value # fails
```

</details>

4. Executing under the following environment

<details>
<summary> Execution </summary>

```bash
$ python app.py
```

</details>

**Expected behavior**
I would expect all documented settings access methods to function properly.

**Environment (please complete the following information):**
 - OS: Linux 5.9.10-arch1-1 #1 SMP PREEMPT Sun, 22 Nov 2020 14:16:59 +0000 x86_64 GNU/Linux
 - Dynaconf Version 3.1.2




Discussion:
It seems that accessing via [__getitem__](https://github.com/rochacbruno/dynaconf/blob/327f5619a42a90fd12a180db2d4fa7811623b22e/dynaconf/base.py#L283) differs from via [__getattr__](https://github.com/rochacbruno/dynaconf/blob/327f5619a42a90fd12a180db2d4fa7811623b22e/dynaconf/base.py#L138). Specifically I see that  `__getattr__` calls `recursively_evaluate_lazy_format` whereas `__getitem__` does not.
 
I think this issue was solved in #405 I will double check.

Resolving patch:
diff --git a/dynaconf/base.py b/dynaconf/base.py
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -281,6 +281,28 @@ def __contains__(self, item):
         """Respond to `item in settings`"""
         return item.upper() in self.store or item.lower() in self.store
 
+    def __getattr__(self, item):
+        """This method is called only when the attribute does not exits
+        usually on access to the direct `Settings` object instead of the
+        `LazySettings`.
+        All first level attributes are stored as UPPERCASE, so self.FOO
+        will always be accessible, however in some cases `self.foo` might
+        be acessible. This method routes this access to the underlying `_store`
+        which handles casing and recursive access.
+        """
+        try:
+            if (
+                item.islower()
+                and self._store.get("LOWERCASE_READ_FOR_DYNACONF", empty)
+                is False
+            ):
+                raise KeyError
+            value = self._store[item]
+        except KeyError:
+            raise AttributeError(f"Settings has no attribute '{item}'")
+        else:
+            return value
+
     def __getitem__(self, item):
         """Allow getting variables as dict keys `settings['KEY']`"""
         value = self.get(item, default=empty)
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
@@ -209,6 +209,7 @@ def evaluate(settings, *args, **kwargs):
 
 
 converters = {
+    "@str": str,
     "@int": int,
     "@float": float,
     "@bool": lambda value: str(value).lower() in true_values,
diff --git a/example/issues/482_layered_format/app.py b/example/issues/482_layered_format/app.py
new file mode 100644
--- /dev/null
+++ b/example/issues/482_layered_format/app.py
@@ -0,0 +1,52 @@
+from dynaconf import Dynaconf
+
+settings_files = ["settings.toml", "other.toml"]
+
+settings = Dynaconf(settings_files=settings_files)
+
+expected_value = "s3a://kewl_bucket"
+
+assert settings.s3_url == expected_value
+assert settings.s3_url1 == expected_value
+assert settings.s3_url2 == expected_value
+assert settings.s3_url3 == expected_value
+
+assert settings["s3_url"] == expected_value
+assert settings["S3_URL"] == expected_value
+
+assert settings.get("s3_url", default="s3://default") == expected_value
+assert settings.get("S3_URL", default="s3://default") == expected_value
+
+assert settings("s3_url", cast="@str") == expected_value
+assert settings("S3_URL", cast="@str") == expected_value
+
+expected = {
+    "first_name": "Bruno",
+    "morenested": {
+        "last_name": "Rocha",
+        "evenmore": {
+            "full_name": "Bruno Rocha",
+            "full_name2": "Bruno Rocha",
+            "full_name3": "Bruno Rocha",
+        },
+    },
+}
+
+assert (
+    settings.nested
+    == settings.NESTED
+    == settings["nested"]
+    == settings["NESTED"]
+    == settings.get("NesTed")
+    == settings("nested")
+)
+
+assert settings.nested.to_dict() == expected, (settings.nested, expected)
+
+print(settings.s3_url)
+print(settings.S3_URL)
+
+print(settings["s3_url"])
+print(settings["S3_URL"])
+
+print(settings.nested)


Test patch: