Repository: dynaconf/dynaconf
Pull request #916 (2023-04-11 18:14:32)

Feature - Allow system environment fallback in `get()` #742
I've changed the proposed name in the original issue from `fallback_to_environ` to `sysenv_fallback` to stress that those are system environment variables, but it is totally fine to switch it back or to another name.

Also, I had some doubts on dealing with mixed casing, because windows seems to uppercase all environment variables and linux don't. To be safe, I chose to implement an uppercased-lookup of system variables, even though linux would support matching mixed-case.

pedro-psb: I'll see if I can do it
rochacbruno: Looks like we are already using the proper version of the action https://github.com/dynaconf/dynaconf/blob/4dcfc79a4f19ea9b36b78f76dc9668bae5d7124e/.github/workflows/main.yml#L92-L98
rochacbruno: we just need to remove codecov from requirements 

```ERROR: No matching distribution found for codecov```
rochacbruno: https://github.com/dynaconf/dynaconf/pull/918
codecov-commenter: ## [Codecov](https://codecov.io/gh/dynaconf/dynaconf/pull/916?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf) Report
> Merging [#916](https://codecov.io/gh/dynaconf/dynaconf/pull/916?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf) (e77225f) into [master](https://codecov.io/gh/dynaconf/dynaconf/commit/c322950303ec4e6f596c6a7fa287aef6d9b368c2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf) (c322950) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

:mega: This organization is not using Codecov’s [GitHub App Integration](https://github.com/apps/codecov). We recommend you install it so Codecov can continue to function properly for your repositories. [Learn more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf)

```diff
@@           Coverage Diff           @@
##           master     #916   +/-   ##
=======================================
  Coverage   99.72%   99.72%           
=======================================
  Files          21       21           
  Lines        1838     1844    +6     
=======================================
+ Hits         1833     1839    +6     
  Misses          5        5           
```


| [Impacted Files](https://codecov.io/gh/dynaconf/dynaconf/pull/916?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf) | Coverage Δ | |
|---|---|---|
| [dynaconf/base.py](https://codecov.io/gh/dynaconf/dynaconf/pull/916?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf#diff-ZHluYWNvbmYvYmFzZS5weQ==) | `100.00% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dynaconf)


Patch:
diff --git a/docs/configuration.md b/docs/configuration.md
index 4d39305..b082812 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -506,6 +506,47 @@ skip_files=["path/to/ignored.toml"]
 
 ---
 
+### **sysenv_fallback**
+
+> type=`bool | list[str]`, default=`False` </br>
+> env-var=`SYSENV_FALLBACK_FOR_DYNACONF`
+
+Defines if dynaconf will try to load system environment variables (unprefixed)
+as a fallback when using `settings.get()`.
+
+Valid options are:
+
+- `False` (default): won't try to load system envvar
+- `True`: will try to load sys envvar for any name requested in `get()`
+- `list[str]`: will try to load sys envvar for the names provided in the list
+
+
+</br>
+
+This behaviour can also be defined on a per-call basis by providing
+the parameter `sysenv_fallback` to `settings.get()`
+
+Eg:
+
+```python
+from dynaconf import Dynaconf
+
+# will look for PATH (uppercase) in the system environment variables
+settings = Dynaconf(sysenv_fallback=True)
+settings.get("path")
+
+# can be set on a per-call basis
+settings = Dynaconf()
+settings.get("path", sysenv_fallback=True)
+
+# if a list is used, it filters what names are allowed.
+settings = Dynaconf(sysenv_fallback=["path"])
+settings.get("path")
+settings.get("user") # won't try loading unprefixed USER
+```
+
+---
+
 ### **validate_on_update**
 
 > type=`False | True | "all"`, default=`False` </br>
diff --git a/dynaconf/base.py b/dynaconf/base.py
index 35e1f85..95e5207 100644
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -424,6 +424,7 @@ class Settings:
         fresh=False,
         dotted_lookup=empty,
         parent=None,
+        sysenv_fallback=None,
     ):
         """
         Get a value from settings store, this is the preferred way to access::
@@ -437,8 +438,12 @@ class Settings:
         :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?
+        :param sysenv_fallback: Should fallback to system environ if not found?
         :return: The value if found, default or None
         """
+        if sysenv_fallback is None:
+            sysenv_fallback = self._store.get("SYSENV_FALLBACK_FOR_DYNACONF")
+
         nested_sep = self._store.get("NESTED_SEPARATOR_FOR_DYNACONF")
         if nested_sep and nested_sep in key:
             # turn FOO__bar__ZAZ in `FOO.bar.ZAZ`
@@ -456,14 +461,23 @@ class Settings:
                 parent=parent,
             )
 
+        key = upperfy(key)
+
+        # handles system environment fallback
+        if default is None:
+            key_in_sysenv_fallback_list = isinstance(
+                sysenv_fallback, list
+            ) and key in [upperfy(k) for k in sysenv_fallback]
+            if sysenv_fallback is True or key_in_sysenv_fallback_list:
+                default = self.environ.get(key)
+
+        # default values should behave exactly Dynaconf parsed values
         if default is not None:
-            # default values should behave exactly Dynaconf parsed values
             if isinstance(default, list):
                 default = BoxList(default)
             elif isinstance(default, dict):
                 default = DynaBox(default)
 
-        key = upperfy(key)
         if key in self._deleted:
             return default
 
diff --git a/dynaconf/default_settings.py b/dynaconf/default_settings.py
index 5fd5692..36c402d 100644
--- a/dynaconf/default_settings.py
+++ b/dynaconf/default_settings.py
@@ -246,8 +246,13 @@ APPLY_DEFAULT_ON_NONE_FOR_DYNACONF = get(
     "APPLY_DEFAULT_ON_NONE_FOR_DYNACONF", None
 )
 
+# Auto trigger validation when Settings update methods are called directly
+# (set, update, load_file)
 VALIDATE_ON_UPDATE_FOR_DYNACONF = get("VALIDATE_ON_UPDATE_FOR_DYNACONF", False)
 
+# Use system environ as fallback when a setting was not set
+SYSENV_FALLBACK_FOR_DYNACONF = get("SYSENV_FALLBACK_FOR_DYNACONF", False)
+
 
 # Backwards compatibility with renamed variables
 for old, new in RENAMED_VARS.items():


Test patch:
diff --git a/tests/test_base.py b/tests/test_base.py
index 0c5adf5..9bd59d3 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -1359,3 +1359,67 @@ def test_load_file__validate_on_update_is_str_all(tmpdir, valid, invalid):
     # should pass
     assert not settings.exists("value_a")
     settings.load_file(file_with_valid)
+
+
+def test_get_with_sysenv_fallback_global_as_false():
+    """
+    When global sysenv_fallback is False
+    Should not fallback to sys envvars (default)
+    """
+    settings = Dynaconf(sysenv_fallback=False)
+    settings.environ["TEST_KEY"] = "TEST_VALUE"
+
+    assert settings.sysenv_fallback_for_dynaconf is False
+    assert not settings.get("test_key")
+
+
+def test_get_with_sysenv_fallback_global_as_true():
+    """
+    When sysenv_fallback is True
+    Should fallback to sys envvars for uppercase envvar-names only
+    """
+    settings = Dynaconf(sysenv_fallback=True)
+    settings.environ["TEST_KEY"] = "TEST_VALUE"
+
+    assert settings.sysenv_fallback_for_dynaconf is True
+    assert settings.get("test_key") == "TEST_VALUE"
+
+
+def test_get_with_sysenv_fallback_global_as_list():
+    """
+    When sysenv_fallback is list
+    Should fallback to sys envvars only for listed names
+    """
+    settings = Dynaconf(sysenv_fallback=["FOO_KEY"])
+    settings.environ["TEST_KEY"] = "TEST_VALUE"
+    settings.environ["FOO_KEY"] = "FOO_VALUE"
+
+    assert isinstance(settings.sysenv_fallback_for_dynaconf, list)
+    assert not settings.get("test_key")
+    assert settings.get("foo_key") == "FOO_VALUE"
+
+
+def test_get_with_sysenv_fallback_local_overrides():
+    """
+    When there are local overrides
+    Should behave according to the overrides
+    """
+    # global is False
+    settings = Dynaconf(sysenv_fallback=False)
+    settings.environ["TEST_KEY"] = "TEST_VALUE"
+
+    assert settings.sysenv_fallback_for_dynaconf is False
+    assert not settings.get("test_key")
+    assert not settings.get("test_key", sysenv_fallback=["foo_key"])
+    assert settings.get("test_key", sysenv_fallback=True) == "TEST_VALUE"
+    assert (
+        settings.get("test_key", sysenv_fallback=["test_key"]) == "TEST_VALUE"
+    )
+
+    # global is True
+    settings = Dynaconf(sysenv_fallback=True)
+    settings.environ["ANOTHER_TEST"] = "ANOTHER_VALUE"
+
+    assert settings.sysenv_fallback_for_dynaconf is True
+    assert settings.get("another_test") == "ANOTHER_VALUE"
+    assert not settings.get("another_test", sysenv_fallback=False)