Repository: rochacbruno/dynaconf
Pull request #337 (2020-05-12 06:10:01)

Add ENVLESS_MODE option
Passing `ENVLESS_MODE_FOR_DYNACONF=True` will make Dynaconf to read all
data from source files without env separation.

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

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

```diff
@@            Coverage Diff            @@
##            master      #337   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           18        18           
  Lines          957       969   +12     
=========================================
+ Hits           957       969   +12     
```


| [Impacted Files](https://codecov.io/gh/rochacbruno/dynaconf/pull/337?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [dynaconf/cli.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/337/diff?src=pr&el=tree#diff-ZHluYWNvbmYvY2xpLnB5) | `100.00% <ø> (ø)` | |
| [dynaconf/loaders/py\_loader.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/337/diff?src=pr&el=tree#diff-ZHluYWNvbmYvbG9hZGVycy9weV9sb2FkZXIucHk=) | `100.00% <ø> (ø)` | |
| [dynaconf/loaders/base.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/337/diff?src=pr&el=tree#diff-ZHluYWNvbmYvbG9hZGVycy9iYXNlLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/rochacbruno/dynaconf/pull/337?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/337?src=pr&el=footer). Last update [f67cb3a...30e0431](https://codecov.io/gh/rochacbruno/dynaconf/pull/337?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/Makefile b/Makefile
index bfbdfa5..8adef78 100644
--- a/Makefile
+++ b/Makefile
@@ -38,6 +38,7 @@ test_examples:
 	cd example/validators/with_toml/;pwd;dynaconf validate
 	cd example/toml_with_secrets/;pwd;python program.py
 	cd example/envs;pwd;python app.py
+	cd example/envless_mode;pwd;python app.py
 	cd example/custom_loader;pwd;python app.py
 	cd example/get_fresh;pwd;python app.py
 	cd example/includes;pwd;python app.py
diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md
index ca08722..425fc02 100644
--- a/docs/guides/configuration.md
+++ b/docs/guides/configuration.md
@@ -42,6 +42,7 @@ It can also be passed as parameters to extensions like `FlaskDynaconf` or set in
     ENV | str | Working environment. | “development” | ENV_FOR_DYNACONF=production
     *FORCE_ENV | str | Force the working environment | None | FORCE_ENV_FOR_DYNACONF=other
     ENV_SWITCHER | str | Variable used to change working env. | ENV_FOR_DYNACONF | ENV_SWITCHER_FOR_DYNACONF=MYPROGRAM_ENV
+    ENVLESS_MODE | str | Ignore env layering and load everything from file. | ENVLESS_MODE | ENVLESS_MODE_FOR_DYNACONF=true
     ENVVAR | str | The envvar which holds the list of settings files. | ‘SETTINGS_FILE_FOR_DYNACONF’ | ENVVAR_FOR_DYNACONF=MYPROGRAM_SETTINGS
     ENVVAR_PREFIX | str | Prefix for exporting parameters as env vars. Example: If your program is called *MYPROGRAM* you may want users to use *MYPROGRAM_FOO=bar* instead of *DYNACONF_FOO=bar* on envvars. | “DYNACONF” | ENVVAR_PREFIX_FOR_DYNACONF=MYPROGRAM (loads MYPROGRAM_VAR) ENVVAR_PREFIX_FOR_DYNACONF=’’ (loads _VAR) ENVVAR_PREFIX_FOR_DYNACONF=false (loads VAR)
     FRESH_VARS | list | A list of vars to be re-loaded on every access. | [] | FRESH_VARS_FOR_DYNACONF=[“HOST”, “PORT”]
diff --git a/docs/guides/usage.md b/docs/guides/usage.md
index 8cc02dd..5f2109a 100644
--- a/docs/guides/usage.md
+++ b/docs/guides/usage.md
@@ -68,6 +68,22 @@ ENV_FOR_DYNACONF=staging python yourapp.py
 
 > **NOTE:** When using [Flask Extension](flask.html) the environment can be changed via `FLASK_ENV` variable and for [Django Extension](django.html) you can use `DJANGO_ENV`.
 
+## Disabling Working environments
+
+In some cases you might want to disable the layered environment separation and just load everything from a file use:
+
+```bash
+export ENVLESS_MODE_FOR_DYNACONF=true
+```
+
+or pass this setting to your own Dynaconf instance
+
+```py
+from dynaconf import LazySettings
+
+settings = LazySettings(ENVLESS_MODE=True)
+```
+
 ## The settings files
 
 > **NOTE:** The settings files are optional. If it is not present, only the values from **environment variables** and enabled external loaders are used (**.env** file is also supported).
diff --git a/dynaconf/base.py b/dynaconf/base.py
index a4a462d..9e88156 100644
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -431,7 +431,7 @@ class Settings(object):
         """Gets the internal mapping of LOADER -> values"""
         return self._loaded_by_loaders
 
-    def from_env(self, env, keep=False, **kwargs):
+    def from_env(self, env="", keep=False, **kwargs):
         """Return a new isolated settings object pointing to specified env.
 
         Example of settings.toml::
@@ -554,6 +554,9 @@ class Settings(object):
     @property
     def current_env(self):
         """Return the current active env"""
+
+        if self.ENVLESS_MODE_FOR_DYNACONF is True:
+            return "envless_mode"
         if self.FORCE_ENV_FOR_DYNACONF is not None:
             return self.FORCE_ENV_FOR_DYNACONF
 
diff --git a/dynaconf/cli.py b/dynaconf/cli.py
index 75475a5..66ae089 100644
--- a/dynaconf/cli.py
+++ b/dynaconf/cli.py
@@ -288,12 +288,12 @@ def init(fileformat, path, env, _vars, _secrets, wg, y, django):
 
     if not y and settings_path and settings_path.exists():  # pragma: no cover
         click.confirm(
-            f"{settings_path} exists do you want to overwrite it?", abort=True,
+            f"{settings_path} exists do you want to overwrite it?", abort=True
         )
 
     if not y and secrets_path and secrets_path.exists():  # pragma: no cover
         click.confirm(
-            f"{secrets_path} exists do you want to overwrite it?", abort=True,
+            f"{secrets_path} exists do you want to overwrite it?", abort=True
         )
 
     if settings_path and settings_data:
diff --git a/dynaconf/default_settings.py b/dynaconf/default_settings.py
index 1f3e43c..b63209d 100644
--- a/dynaconf/default_settings.py
+++ b/dynaconf/default_settings.py
@@ -91,6 +91,13 @@ SETTINGS_FILE_FOR_DYNACONF = get("SETTINGS_FILE_FOR_DYNACONF", default_paths)
 # # ENV SETTINGS
 # # In dynaconf 1.0.0 `NAMESPACE` got renamed to `ENV`
 
+
+# If True Dynaconf will load all variables in the file
+ENVLESS_MODE_FOR_DYNACONF = get("ENVLESSMODE_FOR_DYNACONF", False)
+
+# If True dynaconf will allow access to first level settngs in lower case
+LOWERCASE_READ_FOR_DYNACONF = get("LOWERCASE_READ_FOR_DYNACONF", False)
+
 # The environment variable to switch current env
 ENV_SWITCHER_FOR_DYNACONF = get(
     "ENV_SWITCHER_FOR_DYNACONF", "ENV_FOR_DYNACONF"
@@ -146,8 +153,7 @@ vault_host = get("VAULT_HOST_FOR_DYNACONF", "localhost")
 vault_port = get("VAULT_PORT_FOR_DYNACONF", "8200")
 default_vault = {
     "url": get(
-        "VAULT_URL_FOR_DYNACONF",
-        f"{vault_scheme}://{vault_host}:{vault_port}",
+        "VAULT_URL_FOR_DYNACONF", f"{vault_scheme}://{vault_host}:{vault_port}"
     ),
     "token": get("VAULT_TOKEN_FOR_DYNACONF", None),
     "cert": get("VAULT_CERT_FOR_DYNACONF", None),
diff --git a/dynaconf/loaders/base.py b/dynaconf/loaders/base.py
index e04a233..68d4c1c 100644
--- a/dynaconf/loaders/base.py
+++ b/dynaconf/loaders/base.py
@@ -65,12 +65,18 @@ class BaseLoader(object):
 
         self.obj._loaded_files.extend(files)
 
-        env_list = build_env_list(self.obj, self.env)
+        source_data = self.get_source_date(files)
 
-        # load all envs
-        self._read(files, env_list, silent, key)
+        if self.obj.get("ENVLESS_MODE_FOR_DYNACONF") is True:
+            self._envless_load(source_data, silent, key)
+        else:
+            self._load_all_envs(source_data, silent, key)
 
-    def _read(self, files, envs, silent=True, key=None):
+    def get_source_date(self, files):
+        """Reads each file and returns source data for each file
+        {"path/to/file.ext": {"key": "value"}}
+        """
+        data = {}
         for source_file in files:
             if source_file.endswith(self.extensions):
                 try:
@@ -80,30 +86,46 @@ class BaseLoader(object):
                             "ENCODING_FOR_DYNACONF", "utf-8"
                         ),
                     ) as open_file:
-                        source_data = self.file_reader(open_file)
-                    self.obj.logger.debug(
-                        f"{self.identifier}_loader: {source_file}"
-                    )
+                        content = self.file_reader(open_file)
+                        if content:
+                            data[source_file] = content
+                            self.obj.logger.debug(
+                                f"{self.identifier}_loader: {source_file}"
+                            )
+                        else:  # pragma: no cover
+                            self.obj.logger.debug(
+                                f"{self.identifier}_loader: {source_file}"
+                                " IS EMPTY"
+                            )
                 except IOError:
                     self.obj.logger.debug(
                         f"{self.identifier}_loader: {source_file} "
                         "(Ignored, file not Found)"
                     )
-                    source_data = None
             else:
                 # for tests it is possible to pass string
-                source_data = self.string_reader(source_file)
+                content = self.string_reader(source_file)
+                if content:
+                    data[source_file] = content
+        return data
+
+    def _envless_load(self, source_data, silent=True, key=None):
+        """Load all the keys from each file without env separation"""
+        for source_file, file_data in source_data.items():
+            self._set_data_to_obj(
+                file_data, self.identifier, source_file, key=key
+            )
 
-            if not source_data:
-                continue
+    def _load_all_envs(self, source_data, silent=True, key=None):
+        """Load configs from files separating by each environment"""
+
+        for source_file, file_data in source_data.items():
 
             # env name is checked in lower
-            source_data = {
-                k.lower(): value for k, value in source_data.items()
-            }
+            file_data = {k.lower(): value for k, value in file_data.items()}
 
             # is there a `dynaconf_merge` on top level of file?
-            file_merge = source_data.get("dynaconf_merge")
+            file_merge = file_data.get("dynaconf_merge")
 
             # all lower case for comparison
             base_envs = [
@@ -119,11 +141,11 @@ class BaseLoader(object):
                 "global",
             ]
 
-            for env in envs:
+            for env in build_env_list(self.obj, self.env):
                 env = env.lower()  # lower for better comparison
                 data = {}
                 try:
-                    data = source_data[env] or {}
+                    data = file_data[env] or {}
                 except KeyError:
                     if env not in base_envs:
                         message = (
@@ -141,34 +163,49 @@ class BaseLoader(object):
                 else:
                     identifier = self.identifier
 
-                # data 1st level keys should be transformed to upper case.
-                data = {upperfy(k): v for k, v in data.items()}
-                if key:
-                    key = upperfy(key)
-
-                is_secret = "secret" in source_file
-                _keys = (list(data.keys()) if is_secret else data,)
-                _path = os.path.split(source_file)[-1]
-
-                self.obj.logger.debug(
-                    f"{self.identifier}_loader: {_path}[{env}]{_keys}"
+                self._set_data_to_obj(
+                    data, identifier, source_file, file_merge, key, env
                 )
 
-                # is there a `dynaconf_merge` inside an `[env]`?
-                file_merge = file_merge or data.pop("DYNACONF_MERGE", False)
-
-                if not key:
-                    self.obj.update(
-                        data,
-                        loader_identifier=identifier,
-                        is_secret=is_secret,
-                        merge=file_merge,
-                    )
-                elif key in data:
-                    self.obj.set(
-                        key,
-                        data.get(key),
-                        loader_identifier=identifier,
-                        is_secret=is_secret,
-                        merge=file_merge,
-                    )
+    def _set_data_to_obj(
+        self,
+        data,
+        identifier,
+        source_file,
+        file_merge=None,
+        key=False,
+        env=False,
+    ):
+        """Calls setttings.set to add the keys"""
+
+        # data 1st level keys should be transformed to upper case.
+        data = {upperfy(k): v for k, v in data.items()}
+        if key:
+            key = upperfy(key)
+
+        is_secret = "secret" in source_file
+        _keys = (list(data.keys()) if is_secret else data,)
+        _path = os.path.split(source_file)[-1]
+
+        self.obj.logger.debug(
+            f"{self.identifier}_loader: {_path}[{env}]{_keys}"
+        )
+
+        # is there a `dynaconf_merge` inside an `[env]`?
+        file_merge = file_merge or data.pop("DYNACONF_MERGE", False)
+
+        if not key:
+            self.obj.update(
+                data,
+                loader_identifier=identifier,
+                is_secret=is_secret,
+                merge=file_merge,
+            )
+        elif key in data:
+            self.obj.set(
+                key,
+                data.get(key),
+                loader_identifier=identifier,
+                is_secret=is_secret,
+                merge=file_merge,
+            )
diff --git a/dynaconf/loaders/py_loader.py b/dynaconf/loaders/py_loader.py
index 80b48b8..cc03d24 100644
--- a/dynaconf/loaders/py_loader.py
+++ b/dynaconf/loaders/py_loader.py
@@ -42,7 +42,7 @@ def load_from_python_object(
                     "*****" if "secret" in settings_module else setting_value
                 )
                 obj.logger.debug(
-                    f"py_loader: loading {setting}: {secret} ({identifier})",
+                    f"py_loader: loading {setting}: {secret} ({identifier})"
                 )
                 obj.set(
                     setting,
diff --git a/dynaconf/loaders/vault_loader.py b/dynaconf/loaders/vault_loader.py
index fc87d95..ef9462e 100644
--- a/dynaconf/loaders/vault_loader.py
+++ b/dynaconf/loaders/vault_loader.py
@@ -72,7 +72,7 @@ def load(obj, env=None, silent=None, key=None):
             elif data:
                 obj.logger.debug(
                     f"vault_loader: loading: {list(data.keys())} "
-                    f"({IDENTIFIER}:{path})",
+                    f"({IDENTIFIER}:{path})"
                 )
                 obj.update(data, loader_identifier=IDENTIFIER, tomlfy=True)
         except Exception as e:
diff --git a/example/envless_mode/.env b/example/envless_mode/.env
new file mode 100644
index 0000000..e69de29
diff --git a/example/envless_mode/__init__.py b/example/envless_mode/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/example/envless_mode/app.py b/example/envless_mode/app.py
new file mode 100644
index 0000000..1c44017
--- /dev/null
+++ b/example/envless_mode/app.py
@@ -0,0 +1,11 @@
+import os
+
+from dynaconf import LazySettings
+
+settings = LazySettings(ENVLESS_MODE=True)
+
+
+assert settings.FOO == "bar"
+assert settings.HELLO == "world"
+assert settings.DATABASES.default.port == 8080
+assert settings.LAZY == os.environ["HOME"]
diff --git a/example/envless_mode/settings.yaml b/example/envless_mode/settings.yaml
new file mode 100644
index 0000000..9ec221e
--- /dev/null
+++ b/example/envless_mode/settings.yaml
@@ -0,0 +1,8 @@
+---
+foo: bar
+hello: world
+databases:
+  default:
+    port: 8080
+number: 1
+lazy: "@format {env[HOME]}"


Test patch:
diff --git a/tests/test_base.py b/tests/test_base.py
index 3ccba53..e57d356 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -774,3 +774,20 @@ def test_config_aliases(tmpdir):
     assert settings.INCLUDES_FOR_DYNACONF == []
     assert settings.ENV_FOR_DYNACONF == "awesome"
     assert settings.current_env == "awesome"
+
+
+def test_envless_mode(tmpdir):
+    data = {
+        "foo": "bar",
+        "hello": "world",
+        "default": 1,
+        "databases": {"default": {"port": 8080}},
+    }
+    toml_loader.write(str(tmpdir.join("settings.toml")), data)
+
+    settings = LazySettings(ENVLESS_MODE=True)
+
+    assert settings.FOO == "bar"
+    assert settings.HELLO == "world"
+    assert settings.DEFAULT == 1
+    assert settings.DATABASES.default.port == 8080