Repository: rochacbruno/dynaconf
Pull request #240 (2019-09-25 21:24:03)

Replace key.upper with `upperfy` function that keeps `__` attributes

codecov[bot]: # [Codecov](https://codecov.io/gh/rochacbruno/dynaconf/pull/240?src=pr&el=h1) Report
> Merging [#240](https://codecov.io/gh/rochacbruno/dynaconf/pull/240?src=pr&el=desc) into [master](https://codecov.io/gh/rochacbruno/dynaconf/commit/cf43f519673bfbc7deaf062ea6a9d80735448d3f?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/rochacbruno/dynaconf/pull/240/graphs/tree.svg?width=650&token=GXszVzJcqW&height=150&src=pr)](https://codecov.io/gh/rochacbruno/dynaconf/pull/240?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master   #240   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          18     18           
  Lines         770    775    +5     
=====================================
+ Hits          770    775    +5
```


| [Impacted Files](https://codecov.io/gh/rochacbruno/dynaconf/pull/240?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [dynaconf/utils/\_\_init\_\_.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/240/diff?src=pr&el=tree#diff-ZHluYWNvbmYvdXRpbHMvX19pbml0X18ucHk=) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/rochacbruno/dynaconf/pull/240?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/rochacbruno/dynaconf/pull/240?src=pr&el=footer). Last update [cf43f51...e352d97](https://codecov.io/gh/rochacbruno/dynaconf/pull/240?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/dynaconf/base.py b/dynaconf/base.py
index e68ef08..502edcb 100644
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -19,6 +19,7 @@ from dynaconf.utils import missing
 from dynaconf.utils import object_merge
 from dynaconf.utils import raw_logger
 from dynaconf.utils import RENAMED_VARS
+from dynaconf.utils import upperfy
 from dynaconf.utils.boxing import DynaBox
 from dynaconf.utils.files import find_file
 from dynaconf.utils.functional import empty
@@ -295,7 +296,7 @@ class Settings(object):
                 dotted_key=key, default=default, cast=cast, fresh=fresh
             )
 
-        key = key.upper()
+        key = upperfy(key)
         if key in self._deleted:
             return default
 
@@ -320,7 +321,7 @@ class Settings(object):
         :param fresh: if key should be taken from source direclty
         :return: Boolean
         """
-        key = key.upper()
+        key = upperfy(key)
         if key in self._deleted:
             return False
         return self.get(key, fresh=fresh, default=missing) is not missing
@@ -345,7 +346,7 @@ class Settings(object):
          or cast must be true to use cast inference
         :return: The value if found, default or None
         """
-        key = key.upper()
+        key = upperfy(key)
         data = self.environ.get(key, default)
         if data:
             if cast in converters:
@@ -356,7 +357,7 @@ class Settings(object):
 
     def exists_in_environ(self, key):
         """Return True if env variable is exported"""
-        return key.upper() in self.environ
+        return upperfy(key) in self.environ
 
     def as_bool(self, key):
         """Partial method for get with bool cast"""
@@ -603,7 +604,7 @@ class Settings(object):
         :param key: The key to be unset
         :param force: Bypass default checks and force unset
         """
-        key = key.strip().upper()
+        key = upperfy(key.strip())
         if (
             key not in dir(default_settings)
             and key not in self._defaults
@@ -687,7 +688,7 @@ class Settings(object):
             )
 
         value = parse_conf_data(value, tomlfy=tomlfy)
-        key = key.strip().upper()
+        key = upperfy(key.strip())
         existing = getattr(self, key, None)
 
         if getattr(value, "dynaconf_del", None):
@@ -1009,7 +1010,7 @@ class Settings(object):
         """
         keys = keys or self.keys()
         for key in keys:
-            key = key.upper()
+            key = upperfy(key)
             if ignore and key in ignore:
                 continue
             value = self.get(key, empty)
diff --git a/dynaconf/cli.py b/dynaconf/cli.py
index 9429e86..68ea24b 100644
--- a/dynaconf/cli.py
+++ b/dynaconf/cli.py
@@ -16,6 +16,7 @@ from dynaconf import default_settings
 from dynaconf import LazySettings
 from dynaconf import loaders
 from dynaconf.loaders.py_loader import get_module
+from dynaconf.utils import upperfy
 from dynaconf.utils.files import read_file
 from dynaconf.utils.parse_conf import parse_conf_data
 from dynaconf.validator import ValidationError
@@ -107,7 +108,7 @@ def split_vars(_vars):
     """Splits values like foo=bar=zaz in {'foo': 'bar=zaz'}"""
     return (
         {
-            k.upper().strip(): parse_conf_data(v.strip(), tomlfy=True)
+            upperfy(k.strip()): parse_conf_data(v.strip(), tomlfy=True)
             for k, _, v in [item.partition("=") for item in _vars]
         }
         if _vars
@@ -436,7 +437,7 @@ def _list(env, key, more, loader, _all=False, output=None, flat=False):
         if output:
             loaders.write(output, data, env=not flat and cur_env)
     else:
-        key = key.upper()
+        key = upperfy(key)
         value = data.get(key)
         if not value:
             click.echo(click.style("Key not found", bg="red", fg="white"))
@@ -444,13 +445,13 @@ def _list(env, key, more, loader, _all=False, output=None, flat=False):
         click.echo(
             "%s: %s"
             % (
-                click.style(key.upper(), bg=color(key), fg="white"),
+                click.style(upperfy(key), bg=color(key), fg="white"),
                 pprint.pformat(value),
             )
         )
         if output:
             loaders.write(
-                output, {key.upper(): value}, env=not flat and cur_env
+                output, {upperfy(key): value}, env=not flat and cur_env
             )
 
     if env:
diff --git a/dynaconf/default_settings.py b/dynaconf/default_settings.py
index 9818ed9..1f26a2b 100644
--- a/dynaconf/default_settings.py
+++ b/dynaconf/default_settings.py
@@ -5,6 +5,7 @@ import warnings
 
 from dynaconf.utils import raw_logger
 from dynaconf.utils import RENAMED_VARS
+from dynaconf.utils import upperfy
 from dynaconf.utils import warn_deprecations
 from dynaconf.utils.files import find_file
 from dynaconf.utils.parse_conf import parse_conf_data
@@ -30,7 +31,7 @@ def try_renamed(key, value, older_key, current_key):
 
 
 def get(key, default=None):
-    value = os.environ.get(key.upper())
+    value = os.environ.get(upperfy(key))
 
     # compatibility with renamed variables
     for old, new in RENAMED_VARS.items():
diff --git a/dynaconf/loaders/base.py b/dynaconf/loaders/base.py
index 8162186..3d43330 100644
--- a/dynaconf/loaders/base.py
+++ b/dynaconf/loaders/base.py
@@ -4,6 +4,7 @@ import os
 from dynaconf.utils import build_env_list
 from dynaconf.utils import ensure_a_list
 from dynaconf.utils import raw_logger
+from dynaconf.utils import upperfy
 
 
 logger = raw_logger()
@@ -141,9 +142,9 @@ class BaseLoader(object):
                     identifier = self.identifier
 
                 # data 1st level keys should be transformed to upper case.
-                data = {k.upper(): v for k, v in data.items()}
+                data = {upperfy(k): v for k, v in data.items()}
                 if key:
-                    key = key.upper()
+                    key = upperfy(key)
 
                 is_secret = "secret" in source_file
 
diff --git a/dynaconf/loaders/py_loader.py b/dynaconf/loaders/py_loader.py
index ea7eeaf..261c4f2 100644
--- a/dynaconf/loaders/py_loader.py
+++ b/dynaconf/loaders/py_loader.py
@@ -10,6 +10,7 @@ from dynaconf import default_settings
 from dynaconf.utils import DynaconfDict
 from dynaconf.utils import object_merge
 from dynaconf.utils import raw_logger
+from dynaconf.utils import upperfy
 from dynaconf.utils.files import find_file
 
 
@@ -145,7 +146,7 @@ def write(settings_path, settings_data, merge=True):
     ) as f:
         f.writelines(
             [
-                "{} = {}\n".format(k.upper(), repr(v))
+                "{} = {}\n".format(upperfy(k), repr(v))
                 for k, v in settings_data.items()
             ]
         )
diff --git a/dynaconf/loaders/redis_loader.py b/dynaconf/loaders/redis_loader.py
index 236af2d..6283c30 100644
--- a/dynaconf/loaders/redis_loader.py
+++ b/dynaconf/loaders/redis_loader.py
@@ -1,4 +1,5 @@
 from dynaconf.utils import build_env_list
+from dynaconf.utils import upperfy
 from dynaconf.utils.parse_conf import parse_conf_data
 from dynaconf.utils.parse_conf import unparse_conf_data
 
@@ -89,7 +90,7 @@ def write(obj, data=None, **kwargs):
     if not data:
         raise AttributeError("Data must be provided")
     redis_data = {
-        key.upper(): unparse_conf_data(value) for key, value in data.items()
+        upperfy(key): unparse_conf_data(value) for key, value in data.items()
     }
     client.hmset(holder.upper(), redis_data)
     load(obj)
@@ -108,7 +109,7 @@ def delete(obj, key=None):
     holder = "{0}_{1}".format(holder, obj.current_env.upper())
 
     if key:
-        client.hdel(holder.upper(), key.upper())
+        client.hdel(holder.upper(), upperfy(key))
         obj.unset(key)
     else:
         keys = client.hkeys(holder.upper())
diff --git a/dynaconf/utils/__init__.py b/dynaconf/utils/__init__.py
index ff3e0d8..b53c55d 100644
--- a/dynaconf/utils/__init__.py
+++ b/dynaconf/utils/__init__.py
@@ -229,3 +229,29 @@ def build_env_list(obj, env):
 
     # loaders are responsible to change to lower/upper cases
     return [env.lower() for env in env_list]
+
+
+def upperfy(key):
+    """Receive a string key and returns its upper version.
+
+    Example:
+
+      input: foo
+      output: FOO
+
+      input: foo_bar
+      output: FOO_BAR
+
+      input: foo__bar__ZAZ
+      output: FOO__bar__ZAZ
+
+    Arguments:
+        key {str} -- A string key that may contain dunders `__`
+
+    Returns:
+        The key as upper case but keeping the nested elements.
+    """
+    if "__" in key:
+        parts = key.split("__")
+        return "__".join([parts[0].upper()] + parts[1:])
+    return key.upper()
diff --git a/dynaconf/utils/boxing.py b/dynaconf/utils/boxing.py
index 0a68410..f163b56 100644
--- a/dynaconf/utils/boxing.py
+++ b/dynaconf/utils/boxing.py
@@ -1,5 +1,7 @@
 from box import Box
 
+from dynaconf.utils import upperfy
+
 
 class DynaBox(Box):
     """Specialized Box for dynaconf
@@ -9,14 +11,14 @@ class DynaBox(Box):
         try:
             return super(DynaBox, self).__getattr__(item, *args, **kwargs)
         except (AttributeError, KeyError):
-            n_item = item.lower() if item.isupper() else item.upper()
+            n_item = item.lower() if item.isupper() else upperfy(item)
             return super(DynaBox, self).__getattr__(n_item, *args, **kwargs)
 
     def __getitem__(self, item, *args, **kwargs):
         try:
             return super(DynaBox, self).__getitem__(item, *args, **kwargs)
         except (AttributeError, KeyError):
-            n_item = item.lower() if item.isupper() else item.upper()
+            n_item = item.lower() if item.isupper() else upperfy(item)
             return super(DynaBox, self).__getitem__(n_item, *args, **kwargs)
 
     def __copy__(self):
@@ -28,6 +30,6 @@ class DynaBox(Box):
     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 item.upper()
+            n_item = item.lower() if item.isupper() else upperfy(item)
             value = super(DynaBox, self).get(n_item, default, *args, **kwargs)
         return value


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 42d909a..a87ff5d 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -9,6 +9,7 @@ from dynaconf.utils import Missing
 from dynaconf.utils import missing
 from dynaconf.utils import object_merge
 from dynaconf.utils import trimmed_split
+from dynaconf.utils import upperfy
 from dynaconf.utils.files import find_file
 from dynaconf.utils.files import get_local_filename
 from dynaconf.utils.parse_conf import parse_conf_data
@@ -220,3 +221,14 @@ def test_get_local_filename():
     settings_path = os.path.join("foo", "b", "conf.toml")
     local_path = os.path.join("foo", "b", "conf.local.toml")
     assert get_local_filename(settings_path) == local_path
+
+
+def test_upperfy():
+    assert upperfy("foo") == "FOO"
+    assert upperfy("foo__bar") == "FOO__bar"
+    assert upperfy("foo__bar__ZAZ") == "FOO__bar__ZAZ"
+    assert (
+        upperfy("foo__bar__ZAZ__naz__TAZ_ZAZ") == "FOO__bar__ZAZ__naz__TAZ_ZAZ"
+    )
+    assert upperfy("foo_bar") == "FOO_BAR"
+    assert upperfy("foo_BAR") == "FOO_BAR"
diff --git a/tests/test_yaml_loader.py b/tests/test_yaml_loader.py
index 47cb8d9..ecd4083 100644
--- a/tests/test_yaml_loader.py
+++ b/tests/test_yaml_loader.py
@@ -199,10 +199,10 @@ def test_local_files(tmpdir):
         link: brunorocha.org
       other:
         foo: bar
-      MUSIC:
-        CURRENT:
-          VOLUME: 10
-          TITLE: The Beatles - Strawberry Fields
+      music:
+        current:
+          volume: 10
+          title: The Beatles - Strawberry Fields
     """
     tmpdir.join("settings.yaml").write(settings_file_yaml)
 
@@ -217,8 +217,8 @@ def test_local_files(tmpdir):
         dynaconf_merge: true
       other:
         baz: zaz
-      MUSIC__current__volume: 100
-      MUSIC__current__title: Led Zeppelin - Immigrant Song
+      music__current__volume: 100
+      music__current__title: Led Zeppelin - Immigrant Song
     """
     tmpdir.join("settings.local.yaml").write(local_file_yaml)
 
@@ -244,10 +244,10 @@ def test_explicit_local_files(tmpdir):
         link: brunorocha.org
       other:
         foo: bar
-      MUSIC:
-        CURRENT:
-          VOLUME: 10
-          TITLE: The Beatles - Strawberry Fields
+      music:
+        current:
+          volume: 10
+          title: The Beatles - Strawberry Fields
     """
     tmpdir.join("foo.yaml").write(settings_file_yaml)
 
@@ -262,8 +262,9 @@ def test_explicit_local_files(tmpdir):
         dynaconf_merge: true
       other:
         baz: zaz
-      MUSIC__current__volume: 100
-      MUSIC__current__title: Led Zeppelin - Immigrant Song
+      music__current__volume: 100
+      music__current__title: Led Zeppelin - Immigrant Song
+      music__current__even__inner__element: true
     """
     tmpdir.join("foo.local.yaml").write(local_file_yaml)
 
@@ -278,3 +279,4 @@ def test_explicit_local_files(tmpdir):
     assert conf.OTHER == {"baz": "zaz"}
     assert conf.MUSIC.current.volume == 100
     assert conf.MUSIC.current.title == "Led Zeppelin - Immigrant Song"
+    assert conf.get("music.current.even.inner.element") is True