Repository: rochacbruno/dynaconf (python)

Issue:
[bug] Dynaconf does not support null values in yaml
**Describe the bug**
If you specify a null value for a variable in a yaml config file and when you try to use this variable you will get "variable does not exists" error.

The same issue should appear when using nulls in vault (since it's a JSON).

**To Reproduce**
The following python snippet can be used to reproduce the issue:

```python
from dynaconf.base import Settings
settings = Settings()
settings['name'] = 'Hello'
settings['nulled_name'] = None

print(settings['name'])
print(settings['nulled_name'])
```

The output is:

```
Hello
Traceback (most recent call last):
  File ".../dynaconf/base.py", line 222, in __getitem__
    raise KeyError("{0} does not exists".format(item))
KeyError: 'nulled_name does not exists'
```


Pull request description:
Fix #288 - Nullable values

Patch:
diff --git a/Makefile b/Makefile
index 65139e5..3a9136d 100644
--- a/Makefile
+++ b/Makefile
@@ -91,6 +91,7 @@ test_examples:
 	cd example/issues/228_nested_toml_bool/django_app;pwd;python manage.py test
 	cd example/issues/251_dotted_unexistent;pwd;python app.py
 	cd example/issues/266_envvar_from_env_override;pwd;python app.py
+	cd example/issues/288_null_values;pwd;python app.py
 test_vault:
 	# @cd example/vault;pwd;python write.py
 	docker run --rm --name dynaconf_with_vault -d -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' -p 8200:8200 vault
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 0cd90b4..9e7ef19 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -3,6 +3,10 @@
 # Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more:
 # https://docs.microsoft.com/azure/devops/pipelines/languages/python
 
+pr:
+- master
+- releases/*
+
 trigger:
 - master
 
diff --git a/dynaconf/base.py b/dynaconf/base.py
index 57f9aa7..99b44ba 100644
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -219,9 +219,9 @@ class Settings(object):
 
     def __getitem__(self, item):
         """Allow getting variables as dict keys `settings['KEY']`"""
-        value = self.get(item)
-        if value is None:
-            raise KeyError("{0} does not exists".format(item))
+        value = self.get(item, default=empty)
+        if value is empty:
+            raise KeyError("{0} does not exist".format(item))
         return value
 
     def __setitem__(self, key, value):
diff --git a/dynaconf/loaders/__init__.py b/dynaconf/loaders/__init__.py
index d0a9939..ae7fb03 100644
--- a/dynaconf/loaders/__init__.py
+++ b/dynaconf/loaders/__init__.py
@@ -196,7 +196,7 @@ def write(filename, data, env=None):
         raise IOError("{0} cannot be found.".format(loader_name))
 
     data = DynaBox(data).to_dict()
-    if env and env not in data:
+    if loader is not py_loader and env and env not in data:
         data = {env: data}
 
     loader.write(filename, data, merge=False)
diff --git a/dynaconf/loaders/toml_loader.py b/dynaconf/loaders/toml_loader.py
index 86892b1..17cfc0d 100644
--- a/dynaconf/loaders/toml_loader.py
+++ b/dynaconf/loaders/toml_loader.py
@@ -57,4 +57,15 @@ def write(settings_path, settings_data, merge=True):
         "w",
         encoding=default_settings.ENCODING_FOR_DYNACONF,
     ) as open_file:
-        toml.dump(settings_data, open_file)
+        toml.dump(encode_nulls(settings_data), open_file)
+
+
+def encode_nulls(data):
+    """TOML does not support `None` so this function transforms to '@none '."""
+    if data is None:
+        return "@none "
+    if isinstance(data, dict):
+        return {key: encode_nulls(value) for key, value in data.items()}
+    elif isinstance(data, (list, tuple)):
+        return [encode_nulls(item) for item in data]
+    return data
diff --git a/dynaconf/utils/boxing.py b/dynaconf/utils/boxing.py
index f163b56..1613bd9 100644
--- a/dynaconf/utils/boxing.py
+++ b/dynaconf/utils/boxing.py
@@ -28,8 +28,6 @@ class DynaBox(Box):
         return self.__class__(super(Box, self).copy())
 
     def get(self, item, default=None, *args, **kwargs):
-        value = super(DynaBox, self).get(item, default, *args, **kwargs)
-        if value is None or value == default:
-            n_item = item.lower() if item.isupper() else upperfy(item)
-            value = super(DynaBox, self).get(n_item, default, *args, **kwargs)
-        return value
+        if item not in self:  # toggle case
+            item = item.lower() if item.isupper() else upperfy(item)
+        return super(DynaBox, self).get(item, default, *args, **kwargs)
diff --git a/example/issues/288_null_values/.env b/example/issues/288_null_values/.env
new file mode 100644
index 0000000..e69de29
diff --git a/example/issues/288_null_values/app.py b/example/issues/288_null_values/app.py
new file mode 100644
index 0000000..016ef07
--- /dev/null
+++ b/example/issues/288_null_values/app.py
@@ -0,0 +1,8 @@
+from dynaconf import settings
+
+assert settings["NULLED_VALUE_PYTHON"] is None
+assert settings["nulled_value_json"] is None
+assert settings["nulled_value_yaml"] is None
+assert settings["nulled_value_toml"] is None
+
+print("#288 PASSED")
diff --git a/example/issues/288_null_values/settings.json b/example/issues/288_null_values/settings.json
new file mode 100644
index 0000000..eef4cf1
--- /dev/null
+++ b/example/issues/288_null_values/settings.json
@@ -0,0 +1,5 @@
+{
+    "default": {
+        "nulled_value_json": null
+    }
+}
diff --git a/example/issues/288_null_values/settings.py b/example/issues/288_null_values/settings.py
new file mode 100644
index 0000000..c82da42
--- /dev/null
+++ b/example/issues/288_null_values/settings.py
@@ -0,0 +1,1 @@
+NULLED_VALUE_PYTHON = None
diff --git a/example/issues/288_null_values/settings.toml b/example/issues/288_null_values/settings.toml
new file mode 100644
index 0000000..9a04eee
--- /dev/null
+++ b/example/issues/288_null_values/settings.toml
@@ -0,0 +1,2 @@
+[default]
+nulled_value_toml = "@none "
diff --git a/example/issues/288_null_values/settings.yaml b/example/issues/288_null_values/settings.yaml
new file mode 100644
index 0000000..25a1cb4
--- /dev/null
+++ b/example/issues/288_null_values/settings.yaml
@@ -0,0 +1,2 @@
+default:
+  nulled_value_yaml: null


Test patch:
diff --git a/tests/test_toml_loader.py b/tests/test_toml_loader.py
index 6694798..6dd2609 100644
--- a/tests/test_toml_loader.py
+++ b/tests/test_toml_loader.py
@@ -1,6 +1,7 @@
 import pytest
 
 from dynaconf import LazySettings
+from dynaconf.loaders.toml_loader import encode_nulls
 from dynaconf.loaders.toml_loader import load
 
 settings = LazySettings(ENV_FOR_DYNACONF="PRODUCTION")
@@ -179,3 +180,16 @@ def test_load_dunder():
     load(settings, filename=toml, env="FOO")
     assert settings.COLORS.gray.code == "#CCCCCC"
     assert settings.COLORS.gray.name == "Gray"
+
+
+def test_encode_nulls():
+    assert encode_nulls(None) == "@none "
+    assert encode_nulls([None, None]) == ["@none ", "@none "]
+    assert encode_nulls((None, None)) == ["@none ", "@none "]
+    assert encode_nulls({"nullable": None}) == {"nullable": "@none "}
+    assert encode_nulls(1) == 1
+    assert encode_nulls(1.1) == 1.1
+    assert encode_nulls(True) is True
+    assert encode_nulls(False) is False
+    assert encode_nulls("") == ""
+    assert encode_nulls("text") == "text"