Repository: dynaconf/dynaconf

Issue:
[bug] Exception when getting non existing dotted keys with default value
**Describe the bug**
Using settings.get(key, default) with a non-existing key - depending on the default value - produces an exception.
Am I missing something obvious?

**To Reproduce**
Steps to reproduce the behavior:

1. Having the following folder structure

- app.py
- config.py

2. Having the following config files:
- config.py 

3. Having the following app code:

<details>
<summary> Code </summary>

**app.py**
```python
from dynaconf import settings as s

s.load_file("config.py")

print("As dict:")
print(s.as_dict())

print("Existing key:", s.get("foo.bar"))

print("With 0 as default:", s.get("non.existing.key", 0))

print("With list as default:", s.get("non.existing.key", [1, 2, 3]))

```

**config.py**
```python
FOO = {
  "bar": "hello"
}
```

</details>

4. Executing under the following environment

```bash

$ python
Python 3.7.3 (default, Mar 27 2019, 16:54:48)
[Clang 4.0.1 (tags/RELEASE_401/final)] :: Anaconda, Inc. on darwin

$ pip freeze
certifi==2019.9.11
chardet==3.0.4
Click==7.0
configobj==5.0.6
dynaconf==2.2.0
entrypoints==0.3
flake8==3.7.8
hvac==0.9.5
idna==2.8
mccabe==0.6.1
pycodestyle==2.5.0
pyflakes==2.1.1
python-box==3.4.5
python-dotenv==0.10.3
PyYAML==5.1.2
redis==3.3.11
requests==2.22.0
six==1.12.0
toml==0.10.0
urllib3==1.25.6

$ python app.py
As dict:
{'FOO': {'bar': 'hello'}}
Existing key: hello
With 0 as default: 0
Traceback (most recent call last):
  File "app.py", line 12, in <module>
    print("With list as default:", s.get("non.existing.key", [1, 2, 3]))
  File "/Users/me/Stuff/dynaconf/pyenv/lib/python3.7/site-packages/dynaconf/base.py", line 296, in get
    dotted_key=key, default=default, cast=cast, fresh=fresh
  File "/Users/me/Stuff/dynaconf/pyenv/lib/python3.7/site-packages/dynaconf/base.py", line 275, in _dotted_get
    return self._dotted_get(".".join(keys), default=default, **kwargs)
  File "/Users/me/Stuff/dynaconf/pyenv/lib/python3.7/site-packages/dynaconf/base.py", line 265, in _dotted_get
    result = self.get(name, default=default, **kwargs)
  File "/Users/me/Stuff/dynaconf/pyenv/lib/python3.7/site-packages/dynaconf/base.py", line 312, in get
    data = store.get(key, default)
AttributeError: 'BoxList' object has no attribute 'get'
```

**Expected behavior**
No exception and no dependency on the default value

**Environment (please complete the following information):**
 - OS: Linux/Ubuntu18.04 and MacOS 10.14.6
 - Dynaconf Version: 2.2.0/wheel



Discussion:

Resolving patch:
diff --git a/dynaconf/base.py b/dynaconf/base.py
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -256,26 +256,34 @@ def as_dict(self, env=None, internal=False):
 
     to_dict = as_dict  # backwards compatibility
 
-    def _dotted_get(self, dotted_key, default=None, **kwargs):
+    def _dotted_get(self, dotted_key, default=None, parent=None, **kwargs):
         """
         Perform dotted key lookups and keep track of where we are.
+        :param key: The name of the setting value, will always be upper case
+        :param default: In case of not found it will be returned
+        :param parent: Is there a pre-loaded parent in a nested data?
         """
         split_key = dotted_key.split(".")
         name, keys = split_key[0], split_key[1:]
-        result = self.get(name, default=default, **kwargs)
-        self._memoized = result
+        result = self.get(name, default=default, parent=parent, **kwargs)
 
-        # If we've reached the end, then return result and clear the
-        # memoized data.
-        if not keys or result is default:
-            self._memoized = None
+        # If we've reached the end, or parent key not found, then return result
+        if not keys or result == default:
             return result
 
         # If we've still got key elements to traverse, let's do that.
-        return self._dotted_get(".".join(keys), default=default, **kwargs)
+        return self._dotted_get(
+            ".".join(keys), default=default, parent=result, **kwargs
+        )
 
     def get(
-        self, key, default=None, cast=None, fresh=False, dotted_lookup=True
+        self,
+        key,
+        default=None,
+        cast=None,
+        fresh=False,
+        dotted_lookup=True,
+        parent=None,
     ):
         """
         Get a value from settings store, this is the prefered way to access::
@@ -288,12 +296,17 @@ def get(
         :param cast: Should cast in to @int, @float, @bool or @json ?
         :param fresh: Should reload from loaders store before access?
         :param dotted_lookup: Should perform dotted-path lookup?
+        :param parent: Is there a pre-loaded parent in a nested data?
         :return: The value if found, default or None
         """
 
         if "." in key and dotted_lookup:
             return self._dotted_get(
-                dotted_key=key, default=default, cast=cast, fresh=fresh
+                dotted_key=key,
+                default=default,
+                cast=cast,
+                fresh=fresh,
+                parent=parent,
             )
 
         key = upperfy(key)
@@ -308,8 +321,7 @@ def get(
             self.unset(key)
             self.execute_loaders(key=key)
 
-        store = self._memoized or self.store
-        data = store.get(key, default)
+        data = (parent or self.store).get(key, default)
         if cast:
             data = converters.get(cast)(data)
         return data
diff --git a/example/issues/251_dotted_unexistent/app.py b/example/issues/251_dotted_unexistent/app.py
new file mode 100644
--- /dev/null
+++ b/example/issues/251_dotted_unexistent/app.py
@@ -0,0 +1,26 @@
+from dynaconf import settings
+
+print("As dict:")
+print(settings.as_dict())
+
+assert settings.FOO == {"bar": "hello"}
+assert settings.FOO.bar == "hello"
+
+assert settings.get("FOO") == {"bar": "hello"}
+assert settings.get("foo") == {"bar": "hello"}
+assert settings.get("foo.bar") == "hello", settings.get("foo.bar")
+
+assert settings.get("foo.zaz") is None
+assert settings.get("non.zaz") is None
+assert settings.get("non.zaz", 3) == 3
+print("With list as default 1:")
+assert settings.get("non.existing.key", [1, 2, 3]) == [1, 2, 3]
+assert settings.get("non.zaz", 3) == 3
+
+assert settings.get("foo.bar") == "hello"
+
+print("With 0 as default:")
+assert settings.get("non.existing.key", 0) == 0
+
+print("With list as default 2:")
+assert settings.get("non.existing.key", [4, 5, 6]) == [4, 5, 6]
diff --git a/example/issues/251_dotted_unexistent/settings.py b/example/issues/251_dotted_unexistent/settings.py
new file mode 100644
--- /dev/null
+++ b/example/issues/251_dotted_unexistent/settings.py
@@ -0,0 +1 @@
+FOO = {"bar": "hello"}


Test patch: