Repository: rochacbruno/dynaconf (python)

Issue:
Missing an isolated namespace [RFC]
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

The Settings object should have a way of accessing and traversing the keys loaded from the settings files without having all of the Dynaconf options in there. When I call `config.keys()` I'm getting `['SETTINGS_MODULE', 'PROJECT_ROOT_FOR_DYNACONF', 'SETTINGS_MODULE_FOR_DYNACONF', ...`

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

Ideally when you call `keys()` and similar methods, it should only return values in the namespaces loaded by the file. Even if it involved doing this `config.current_env.keys()` it would be fine.

Also, having methods of exporting the current (possibly modified) settings would be nice. Basically I would like to be able to do this:

```python 
with open(output_file, 'w') as f:
    yaml.dump(config.environments['development'].as_dict(), f)
```

And have that write out a flattened version of the development namespace.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


Pull request:
Fix #189 - added settings.as_dict() and dynaconf list -o file.json

Patch:
diff --git a/docs/guides/accessing_values.md b/docs/guides/accessing_values.md
index 59396bc..f800704 100644
--- a/docs/guides/accessing_values.md
+++ b/docs/guides/accessing_values.md
@@ -91,3 +91,25 @@ connect(
     passwd=settings.MYSQL.auth.get('passwd'),
 )
 ```
+
+## Export settings as a Python dictionary
+
+After exporting the settings to a python dictionary it is easy to use it to serialize as a JSON, YAML or any other format you may need.
+
+### Programmatically
+
+```py
+from dynaconf import settings
+settings.as_dict()  # a dict with only user defined values in current env
+settings.as_dict(env='production')  # a dict with only user defined values in production env
+settings.as_dict(internal=True)  # a dict with all values, user defined and dynaconf internal
+```
+
+### CLI (export to json)
+
+from your project root folder (generally the same place where you have `.env` or from where you call your scripts.
+
+```bash
+dynaconf list -o path/to/file.json
+dynaconf list -e production -o path/to/file.json
+```
diff --git a/docs/guides/cli.md b/docs/guides/cli.md
index b29135b..b0754a1 100644
--- a/docs/guides/cli.md
+++ b/docs/guides/cli.md
@@ -100,23 +100,28 @@ Options:
                                   e.g: `dynaconf init -s TOKEN=kdslmflds
   --wg / --no-wg
   -y
+  --django TEXT
   --help                          Show this message and exit.
+
 ```
 
 ## dynaconf list
 
-List all defined parameters
+List all defined parameters and optionally export to a json file.
 
 ```
 Usage: dynaconf list [OPTIONS]
 
-  Lists all defined config values
+  Lists all user defined config values and if `--all` is passed it also
+  shows dynaconf internal variables.
 
 Options:
   -e, --env TEXT     Filters the env to get the values
   -k, --key TEXT     Filters a single key
   -m, --more         Pagination more|less style
   -l, --loader TEXT  a loader identifier to filter e.g: toml|yaml
+  -a, --all          show dynaconf internal settings?
+  -o, --output FILE  Filepath to write the listed values as json
   --help             Show this message and exit.
 ```
 
diff --git a/dynaconf/base.py b/dynaconf/base.py
index 7f77cac..d30278e 100644
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -2,6 +2,7 @@ import glob
 import importlib
 import os
 from contextlib import contextmanager
+from contextlib import suppress
 
 from dynaconf import default_settings
 from dynaconf.loaders import default_loader
@@ -233,6 +234,21 @@ class Settings(object):
         """Redirects to store object"""
         return self.store.values()
 
+    def as_dict(self, env=None, internal=False):
+        """Returns a dictionary with set key and values.
+
+        :param env: Str env name, default self.current_env `DEVELOPMENT`
+        :param internal: bool - should include dynaconf internal vars?
+        """
+        ctx_mgr = suppress() if env is None else self.using_env(env)
+        with ctx_mgr:
+            data = self.store.copy()
+            # if not internal remove internal settings
+            if not internal:
+                for name in dir(default_settings):
+                    data.pop(name, None)
+            return data
+
     def _dotted_get(self, dotted_key, default=None, **kwargs):
         """
         Perform dotted key lookups and keep track of where we are.
diff --git a/dynaconf/cli.py b/dynaconf/cli.py
index b62d87e..381ce88 100644
--- a/dynaconf/cli.py
+++ b/dynaconf/cli.py
@@ -1,5 +1,6 @@
 import importlib
 import io
+import json
 import os
 import pprint
 import sys
@@ -372,7 +373,14 @@ def init(fileformat, path, env, _vars, _secrets, wg, y, django):
     is_flag=True,
     help="show dynaconf internal settings?",
 )
-def _list(env, key, more, loader, _all=False):
+@click.option(
+    "--output",
+    "-o",
+    type=click.Path(writable=True, dir_okay=False),
+    default=None,
+    help="Filepath to write the listed values as json",
+)
+def _list(env, key, more, loader, _all=False, output=None):
     """Lists all user defined config values
     and if `--all` is passed it also shows dynaconf internal variables.
     """
@@ -398,7 +406,7 @@ def _list(env, key, more, loader, _all=False):
     )
 
     if not loader:
-        data = settings.store
+        data = settings.as_dict(env=env, internal=_all)
     else:
         identifier = "{}_{}".format(loader, cur_env)
         data = settings._loaded_by_loaders.get(identifier, {})
@@ -407,11 +415,6 @@ def _list(env, key, more, loader, _all=False):
     # remove to avoid displaying twice
     data.pop("SETTINGS_MODULE", None)
 
-    # if not --all remove internal settings
-    if not _all:
-        for name in dir(default_settings):
-            data.pop(name, None)
-
     def color(_k):
         if _k in dir(default_settings):
             return "blue"
@@ -424,6 +427,9 @@ def _list(env, key, more, loader, _all=False):
             for k, v in data.items()
         )
         (click.echo_via_pager if more else click.echo)(datalines)
+        if output:
+            with open(output, "w") as output_file:
+                json.dump({cur_env: data}, output_file)
     else:
         key = key.upper()
         value = data.get(key)
@@ -437,6 +443,9 @@ def _list(env, key, more, loader, _all=False):
                 pprint.pformat(value),
             )
         )
+        if output:
+            with open(output, "w") as output_file:
+                json.dump({cur_env: {key.upper(): value}}, output_file)
 
     if env:
         settings.setenv()


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 41dd10c..7570058 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,10 +1,8 @@
-import io
-import os
+import json
 from pathlib import Path
 
 import pytest
 from click.testing import CliRunner
-from dotenv import cli as dotenv_cli
 
 from dynaconf import default_settings
 from dynaconf import LazySettings
@@ -90,6 +88,22 @@ def test_list(testdir):
     assert "GLOBAL_ENV_FOR_DYNACONF: 'DYNACONF'" not in result
 
 
+def test_list_export_json(testdir):
+    result = run(
+        ["list", "-o", "sets.json"], env={"ROOT_PATH_FOR_DYNACONF": testdir}
+    )
+    assert "DOTENV_STR: 'hello'" in result
+    assert "DOTENV_STR" in read_file("sets.json")
+    assert (
+        json.loads(read_file("sets.json"))["development"]["DOTENV_STR"]
+        == "hello"
+    )
+    assert (
+        json.loads(read_file("sets.json"))["development"]["DOTENV_FLOAT"]
+        == 4.2
+    )
+
+
 def test_list_with_all(testdir):
     """Test list command with --all includes interval vars"""
     result = run(["list", "-a"], env={"ROOT_PATH_FOR_DYNACONF": testdir})
@@ -144,6 +158,18 @@ def test_list_with_key():
     assert "DOTENV_STR: 'hello'" in result
 
 
+def test_list_with_key_export_json(tmpdir):
+    result = run(["list", "-k", "DOTENV_STR", "-o", "sets.json"])
+    assert "DOTENV_STR: 'hello'" in result
+    assert "DOTENV_STR" in read_file("sets.json")
+    assert (
+        json.loads(read_file("sets.json"))["development"]["DOTENV_STR"]
+        == "hello"
+    )
+    with pytest.raises(KeyError):
+        json.loads(read_file("sets.json"))["development"]["DOTENV_FLOAT"]
+
+
 def test_list_with_missing_key():
     result = run(["list", "-k", "NOTEXISTS"])
     assert "Key not found" in result