Repository: dynaconf/dynaconf

Issue:
Add the ability to load only a specified list of keys from envvars
**Describe the bug**
Migrating from other configuration systems I'd need to configure `envvar_prefix` to empty string (`""`) so I can load any environment variable I used before. But it doesn't seem to work with _dynaconf_.

**To Reproduce**
Steps to reproduce the behavior:

1. Create a basic dynaconf configuration

In `config.py`:

```py
from dynaconf import Dynaconf

settings = Dynaconf(
    # This is the important line.
    envvar_prefix="",
)

print(settings.FOO)
```

2. Now create some settings file.

In `settings.py`:
```py
FOO = 'bar'
```

3. Finally try running.
```sh
export SETTINGS_FILE_FOR_DYNACONF=settings
FOO='ham' python config.py
```

You will get:
```
bar
```

**Expected behavior**

The print output states:
```
ham
```

This means the value from environment variable has overridden the default value from `settings.py` module

**Environment (please complete the following information):**
 - OS: Ubuntu Linux 20.04
 - Python version: 3.8.5
 - Dynaconf version: 3.1.2
 - Frameworks in use: none.

**Additional context**
Interestingly running in a "hacky" way changes the variable value:
```sh
_FOO='ham' python config.py
```

This is not what I'd expect though. The point is to modify the bare `FOO` variable.


Discussion:
We need to update the documentation, this should be set to `False` instead of an empty str.

app.py
```py
from dynaconf import Dynaconf

settings = Dynaconf(
    settings_file="settings.py",
    envvar_prefix=False  #  <-- `False`
)

print(settings.FOO)
```

settings.py
```py
FOO = "original"
```

On console:

```
❯ python app.py
original
```

or

```
❯ FOO=changed python app.py
changed
```
Thanks @rochacbruno. Works like a charm.
@rochacbruno 
I noticed, using `envvar_prefix=False` causes Dynaconf to load absolutely all environment variables into the result `LazySettings`, e.g.: `PATH` / `HOME` and other standard environment variables.

Is there any way to narrow them down to only the "known ones", that is explicitly defined in `settings_file` / `settings_files` and `preload` and `includes` ?
@odiroot we dont have that feature (yet)

That is the reason the `DYNACONF_` prefix exists, but I think your idea would be easy to implement, we can add a flag to filter the env_loader to load only existing keys already present in the dictionary,
How would we name it?

```py
settings = Dynaconf(.., load_only_existing_keys_from_envvars=True)
```
Or maybe we can add a composition design

```py
from dynaconf import Dynaconf, Prefix

settings = Dynaconf(..., envvar_prefix=Prefix("myprogram))  # a bare string will still be accepted
settings = Dynaconf(..., envvar_prefix=Prefix(False, existing_only=True))  # custom attributes
```
@rochacbruno 
> That is the reason the DYNACONF_ prefix exists, 

I sort of expected that.

> [..] would be easy to implement, we can add a flag to filter the env_loader to load only existing keys already present in the dictionary,

That's a very good solution.

> How would we name it?
> 
> ```python
> settings = Dynaconf(.., load_only_existing_keys_from_envvars=True)
> ```
> 

I like this approach more. Your option name would be fine, IMHO. If you prefer something shorter maybe `ignore_unknown_envvars`  would work?

So `ignore_unknown_envvars = False` (default, backwards-compatible behaviour) would pull in all envvars (still accounting for any potential prefix).


@rochacbruno If you could point me at a place where environment variables are actually plucked into Dynaconf I would attempt to deliver a solution.
First you need to add the flag, any new variable on `default_settings.py` named in the pattern `IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF` will be a flag in `Dynaconf(.., ignore_unknown_envvars=True)

Here:

https://github.com/rochacbruno/dynaconf/blob/master/dynaconf/default_settings.py#L73-L222

Then you can access the variable using `obj.get('IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF')` here where the envvars are loaded.

https://github.com/rochacbruno/dynaconf/blob/master/dynaconf/loaders/env_loader.py#L24-L63
@odiroot ^
test case can be added here: https://github.com/rochacbruno/dynaconf/blob/master/tests/test_env_loader.py

and optionally a functional example in the example folder like this one https://github.com/rochacbruno/dynaconf/tree/master/example/envvar_prefix

Resolving patch:
diff --git a/dynaconf/default_settings.py b/dynaconf/default_settings.py
--- a/dynaconf/default_settings.py
+++ b/dynaconf/default_settings.py
@@ -113,6 +113,15 @@ def reload(load_dotenv=None, *args, **kwargs):
 # This namespace is used for files and also envvars
 ENVVAR_PREFIX_FOR_DYNACONF = get("ENVVAR_PREFIX_FOR_DYNACONF", "DYNACONF")
 
+# By default all environment variables (filtered by `envvar_prefix`) will
+# be pulled into settings space. In case some of them are polluting the space,
+# setting this flag to `True` will change this behaviour.
+# Only "known" variables will be considered -- that is variables defined before
+# in settings files (or includes/preloads).
+IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF = get(
+    "IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF", False
+)
+
 # The default encoding to open settings files
 ENCODING_FOR_DYNACONF = get("ENCODING_FOR_DYNACONF", "utf-8")
 
diff --git a/dynaconf/loaders/env_loader.py b/dynaconf/loaders/env_loader.py
--- a/dynaconf/loaders/env_loader.py
+++ b/dynaconf/loaders/env_loader.py
@@ -40,6 +40,7 @@ def load_from_env(
         prefix = prefix.upper()
         env_ = f"{prefix}_"
 
+    # Load a single environment variable explicitly.
     if key:
         key = upperfy(key)
         value = environ.get(f"{env_}{key}")
@@ -50,15 +51,26 @@ def load_from_env(
                 obj[key] = parse_conf_data(
                     value, tomlfy=True, box_settings=obj
                 )
+
+    # Load environment variables in bulk (when matching).
     else:
+        # Only known variables should be loaded from environment.
+        ignore_unknown = obj.get('IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF')
+        known_keys = set(obj.store)
+
         trim_len = len(env_)
         data = {
             key[trim_len:]: parse_conf_data(
                 data, tomlfy=True, box_settings=obj
             )
             for key, data in environ.items()
-            if key.startswith(env_)
+            if key.startswith(env_) and not (
+                # Ignore environment variables that haven't been
+                # pre-defined in settings space.
+                ignore_unknown and key[trim_len:] not in known_keys
+            )
         }
+        # Update the settings space based on gathered data from environment.
         if data:
             obj.update(data, loader_identifier=identifier)
 
diff --git a/example/ignore_unknown_envvars/app.py b/example/ignore_unknown_envvars/app.py
new file mode 100644
--- /dev/null
+++ b/example/ignore_unknown_envvars/app.py
@@ -0,0 +1,25 @@
+from dynaconf import LazySettings
+
+
+# No prefix.
+settings = LazySettings(
+    settings_file="settings.toml",
+    load_dotenv=True,
+    ignore_unknown_envvars=True,
+    envvar_prefix=False,
+)
+
+assert settings.VAR1 == "bar"
+assert not settings.get("SECRET")
+
+
+# MYAPP_ prefix.
+settings = LazySettings(
+    settings_file="settings.toml",
+    load_dotenv=True,
+    ignore_unknown_envvars=True,
+    envvar_prefix="MYAPP",
+)
+
+assert settings.VAR1 == "ham"
+assert not settings.get("SECRET")


Test patch:
diff --git a/tests/test_env_loader.py b/tests/test_env_loader.py
--- a/tests/test_env_loader.py
+++ b/tests/test_env_loader.py
@@ -22,7 +22,7 @@
 environ["DYNACONF_VALUE"] = "@float 42.1"
 
 # environ['FRESH_VARS_FOR_DYNACONF'] = '@json ["MUSTBEALWAYSFRESH"]'
-# settings.configure()
+# settings.configure(IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF=True)
 settings.configure(
     FRESH_VARS_FOR_DYNACONF=["MUSTBEALWAYSFRESH"],
     ROOT_PATH_FOR_DYNACONF=os.path.dirname(os.path.abspath(__file__)),
@@ -439,3 +439,53 @@ def test_load_dunder(clean_env):
         silent=True,
     )
     assert "DATABASES" not in settings
+
+
+def test_filtering_unknown_variables():
+    # Predefine some known variable.
+    settings.MYCONFIG = "bar"
+    # Enable environment filtering.
+    settings.IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF = True
+
+    # Pollute the environment.
+    environ["IGNOREME"] = "foo"
+
+    load_from_env(
+        obj=settings,
+        prefix=False,
+        key=None,
+        silent=True,
+        identifier="env_global",
+        env=False,
+    )
+
+    # Verify the filter works.
+    assert not settings.get("IGNOREME")
+    # Smoke test.
+    assert settings.get("MYCONFIG") == "bar"
+
+
+def test_filtering_unknown_variables_with_prefix():
+    # Predefine some known variable.
+    settings.MYCONFIG = "bar"
+    # Enable environment filtering.
+    settings.IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF = True
+
+    # Pollute the environment.
+    environ["APP_IGNOREME"] = "foo"
+    # Also change legitimate variable.
+    environ["APP_MYCONFIG"] = "ham"
+
+    load_from_env(
+        obj=settings,
+        prefix="APP",
+        key=None,
+        silent=True,
+        identifier="env_global",
+        env=False,
+    )
+
+    # Verify the filter works.
+    assert not settings.get("IGNOREME")
+    # Smoke test.
+    assert settings.get("MYCONFIG") == "ham"