Repository: rochacbruno/dynaconf
Pull request #625 (2021-07-23 16:55:01)

feat: add filter strategy with PrefixFilter
Related to: #621

This adds a settings_file_prefix option for toml, json, ini and yaml files which works similarly to envvar_prefix, however it defaults to None and does not filter out any keys if it is not set.

If envs are in use it will do the filtering within each env's variables, an example is shown below

```
[default]
PREFIX_EXTRA = "this will supercede if others are set without prefix"

[development]
PREFIX_HOST = "https://example.com/development"
PREFIX_PORT = "8080"
COMMENT = "this will not show when prefix is used"
EXTRA = "will not show"

[production]
PREFIX_HOST = "https://example.com/production"
PORT = "443"
EXTRA = "will not show"
```

When these are loaded up using the `settings_file_prefix="PREFIX"` option you'll get the following:

```
>>> settings.HOST
'https://example.com/development'
>>> settings.PORT
'8080'
>>> settings.COMMENT
AttributeError: 'Settings' object has no attribute 'COMMENT'
```

When switching to production env:
```
>>> with settings.using_env("production"):
...     settings.HOST
...     settings.EXTRA
...     settings.PORT

'https://example.com/production'
'this will supercede if others are set without prefix'
AttributeError: Attribute PORT was deleted, or belongs to different env
```

I think this behaves as you'd expect, looking forward to some feedback as I had to fumble about a bit to learn how everything fits together so no doubt I've done some less than ideal things


Envless load when environments exist in the file will not work in this way as we'd not be able to detect that `settings.PRODUCTION` is an env to filter the prefix from children. I think this is probably expected if loading envless while envs are included in the file - but if it was possible to check if `ENVIRONMENTS_FOR_DYNACONF` is False *and* detect that the top level key is an environment (`[production]`) then it may be possible, but I could not see an obvious way to detect that `production` is an environment



pep8speaks: Hello @zzZIMAWAKE! Thanks for opening this PR. We checked the lines you've touched for [PEP 8](https://www.python.org/dev/peps/pep-0008) issues, and found:













* In the file [`example/toml_example/app.py`](https://github.com/rochacbruno/dynaconf/blob/f5dba49b3a14c1fff7c99e57d6f9ccddd2cd02f5/example/toml_example/app.py):

> [Line 92:80](https://github.com/rochacbruno/dynaconf/blob/f5dba49b3a14c1fff7c99e57d6f9ccddd2cd02f5/example/toml_example/app.py#L92): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (98 > 79 characters)

* In the file [`example/yaml_example/settings_file/app.py`](https://github.com/rochacbruno/dynaconf/blob/f5dba49b3a14c1fff7c99e57d6f9ccddd2cd02f5/example/yaml_example/settings_file/app.py):

> [Line 85:80](https://github.com/rochacbruno/dynaconf/blob/f5dba49b3a14c1fff7c99e57d6f9ccddd2cd02f5/example/yaml_example/settings_file/app.py#L85): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (107 > 79 characters)
> [Line 88:80](https://github.com/rochacbruno/dynaconf/blob/f5dba49b3a14c1fff7c99e57d6f9ccddd2cd02f5/example/yaml_example/settings_file/app.py#L88): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (84 > 79 characters)












codecov[bot]: # [Codecov](https://codecov.io/gh/rochacbruno/dynaconf/pull/625?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha) Report
> Merging [#625](https://codecov.io/gh/rochacbruno/dynaconf/pull/625?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha) (7befbeb) into [master](https://codecov.io/gh/rochacbruno/dynaconf/commit/467be90ccdf664e4716233ddc18b5c6501cab80d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha) (467be90) will **decrease** coverage by `0.19%`.
> The diff coverage is `88.46%`.

[![Impacted file tree graph](https://codecov.io/gh/rochacbruno/dynaconf/pull/625/graphs/tree.svg?width=650&height=150&src=pr&token=GXszVzJcqW&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha)](https://codecov.io/gh/rochacbruno/dynaconf/pull/625?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha)

```diff
@@             Coverage Diff             @@
##            master     #625      +/-   ##
===========================================
- Coverage   100.00%   99.80%   -0.20%     
===========================================
  Files           20       20              
  Lines         1530     1556      +26     
===========================================
+ Hits          1530     1553      +23     
- Misses           0        3       +3     
```


| [Impacted Files](https://codecov.io/gh/rochacbruno/dynaconf/pull/625?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha) | Coverage Δ | |
|---|---|---|
| [dynaconf/base.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha#diff-ZHluYWNvbmYvYmFzZS5weQ==) | `99.76% <50.00%> (-0.24%)` | :arrow_down: |
| [dynaconf/loaders/base.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha#diff-ZHluYWNvbmYvbG9hZGVycy9iYXNlLnB5) | `97.61% <87.50%> (-2.39%)` | :arrow_down: |
| [dynaconf/loaders/ini\_loader.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha#diff-ZHluYWNvbmYvbG9hZGVycy9pbmlfbG9hZGVyLnB5) | `100.00% <100.00%> (ø)` | |
| [dynaconf/loaders/json\_loader.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha#diff-ZHluYWNvbmYvbG9hZGVycy9qc29uX2xvYWRlci5weQ==) | `100.00% <100.00%> (ø)` | |
| [dynaconf/loaders/toml\_loader.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha#diff-ZHluYWNvbmYvbG9hZGVycy90b21sX2xvYWRlci5weQ==) | `100.00% <100.00%> (ø)` | |
| [dynaconf/loaders/yaml\_loader.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha#diff-ZHluYWNvbmYvbG9hZGVycy95YW1sX2xvYWRlci5weQ==) | `100.00% <100.00%> (ø)` | |
| [dynaconf/utils/\_\_init\_\_.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha#diff-ZHluYWNvbmYvdXRpbHMvX19pbml0X18ucHk=) | `100.00% <0.00%> (ø)` | |
| [dynaconf/utils/parse\_conf.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha#diff-ZHluYWNvbmYvdXRpbHMvcGFyc2VfY29uZi5weQ==) | `100.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/rochacbruno/dynaconf/pull/625?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/rochacbruno/dynaconf/pull/625?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha). Last update [467be90...7befbeb](https://codecov.io/gh/rochacbruno/dynaconf/pull/625?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha).

zzZIMAWAKE: I've got to look in to the issue I'm seeing with one of the examples and recursion depth - will take a look at that tomorrow most likely

Will also add the code coverage that I'm missing, happy to receive any feedback now as well if you fancy it :)
zzZIMAWAKE: Not sure what is happening with the recursive limit being reached for the example: 482_layered_format - it does it for me locally on both master & this branch, have I genuinely broken something?
zzZIMAWAKE: @rochacbruno This is ready for review, the some parts of the pipeline are taking a while as they are queued but everything seems to pass successfully now.
rochacbruno: I like this feature, but now that I was able to fully understand its purpose I have some comments:

1. Naming

 `settings_file_prefix` as the name for argument can be confusing, in a first glance it may look like the prefix for a file name instead of variables inside the file.

2. Make it more composable?


Imagine if on the user standing point it can be this:

`dynaconf/strategies/filtering.py`  **new module**
```py

class PrefixFilter:

    def __init__(self, prefix):
        if not isinstance(prefix, str):
            raise TypeError("`SETTINGS_FILE_PREFIX` must be str")
        prefix = "{}_".format(upperfy(prefix))

    def __call__(self, data, settings):
        """Filter incoming data by prefix"""
        prefix = self.prefix
        return {
                upperfy(
                    k[len(prefix):]
                ): v for k, v in data.items()
                if upperfy(k[:len(prefix)]) == prefix
         }

```

on `dynaconf/loaders/base.py` instead of passing `prefix` around as argument we can have the `obj` to hold the `filter_strategy` as an instance of that class.


```py

from dynaconf import Dynaconf
from dynaconf.strategies.filtering import PrefixFilter

settings = Dynaconf(
    settings_file="settings.toml",
    filter_strategy= PrefixFilter("myprefix")
)

```

That `self.filter_strategy` will be a callable that can be used on https://github.com/rochacbruno/dynaconf/pull/625/files#diff-4d2335a3a5b1b1e4cf1f0d9490c21ed91df851ce973b420729a385d372906eb1R167

@zzZIMAWAKE what do you think?

It can accommodate your use case and allow users to be able to customize the filtering with custom classes or any callable that takes `data, settings` arguments.
rochacbruno: @zzZIMAWAKE I have been thinking in a way to let users to alternatively pass `environments=["prod", "dev", "qa"]` a list of environments explicitly and then "default" and "global" would be automatically part of the environment sets.

It would still accept `True` to load environments arbitrarily but also allow user to specify it. (maybe this is something to be tackled in a different issue/PR)
zzZIMAWAKE: Would that also be allowed to be passed when `environments=False`? What would be the behaviour in that case? The issue I'm having is that if someone tried to use:

```
from dynaconf import Dynaconf
from dynaconf.strategies.filtering import PrefixFilter

settings = Dynaconf(
    settings_file="settings.toml",
    environments=False
    filter_strategy= PrefixFilter("myprefix")
)
```
with a file that contained:

```
[production]
PREFIX_VAR = TEST
```
It will return blank as it will filter out `production` as it doesn't begin with the prefix so `settings.PRODUCTION.VAR` will not exist as `settings.PRODUCTION` will not even exist. Perhaps this is an edge case that is okay with the new strategy as the user could set up their own filtering strategy.

@rochacbruno Have updated to make use of `filtering_strategy` if you'd like to take a look :)

rochacbruno: LGTM  at a forst look @zzZIMAWAKE this is on my list to take deeper a look later today or tomorrow, I will reply here. Thanks again.

Patch:
diff --git a/docs/configuration.md b/docs/configuration.md
index d88666f..605c7ff 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -407,6 +407,16 @@ Read more on [settings_files](/settings_files/)
 
 ---
 
+### **filtering_strategy**
+
+Callable accepting data to be filtered, inbuilts currently include [PrefixFilter](/dynaconf/strategies/filtering.py)
+
+
+- type: class
+- default: None
+
+---
+
 ### **skip_files**
 
 When using a glob on `includes` you might want dynaconf to ignore some files that matches.
diff --git a/dynaconf/base.py b/dynaconf/base.py
index f550c22..0d6bd0c 100644
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -35,7 +35,7 @@ from dynaconf.vendor.box.box_list import BoxList
 
 
 class LazySettings(LazyObject):
-    """Loads settings lazily from multple sources::
+    """Loads settings lazily from multiple sources::
 
         settings = Dynaconf(
             settings_files=["settings.toml"],  # path/glob
@@ -200,6 +200,7 @@ class Settings:
         self._defaults = DynaBox(box_settings=self)
         self.environ = os.environ
         self.SETTINGS_MODULE = None
+        self.filter_strategy = kwargs.get("filter_strategy", None)
         self._not_installed_warnings = []
         self._validate_only = kwargs.pop("validate_only", None)
         self._validate_exclude = kwargs.pop("validate_exclude", None)
@@ -538,6 +539,10 @@ class Settings:
             if key not in RENAMED_VARS
         }
 
+        if self.filter_strategy:
+            # Retain the filtering strategy when switching environments
+            new_data["filter_strategy"] = self.filter_strategy
+
         # This is here for backwards compatibility
         # To be removed on 4.x.x
         default_settings_paths = self.get("default_settings_paths")
@@ -1177,6 +1182,7 @@ RESERVED_ATTRS = (
         "_warn_dynaconf_global_settings",
         "environ",
         "SETTINGS_MODULE",
+        "filter_strategy",
         "validators",
         "_validate_only",
         "_validate_exclude",
diff --git a/dynaconf/loaders/base.py b/dynaconf/loaders/base.py
index 2b355d2..cf3b7b6 100644
--- a/dynaconf/loaders/base.py
+++ b/dynaconf/loaders/base.py
@@ -46,6 +46,7 @@ class BaseLoader:
         :param key: if provided load a single key
         :param silent: if load erros should be silenced
         """
+
         filename = filename or self.obj.get(self.identifier.upper())
         if not filename:
             return
@@ -100,7 +101,11 @@ class BaseLoader:
     def _envless_load(self, source_data, silent=True, key=None):
         """Load all the keys from each file without env separation"""
         for file_data in source_data.values():
-            self._set_data_to_obj(file_data, self.identifier, key=key)
+            self._set_data_to_obj(
+                file_data,
+                self.identifier,
+                key=key,
+            )
 
     def _load_all_envs(self, source_data, silent=True, key=None):
         """Load configs from files separating by each environment"""
@@ -115,7 +120,6 @@ class BaseLoader:
 
             for env in build_env_list(self.obj, self.env):
                 env = env.lower()  # lower for better comparison
-                data = {}
 
                 try:
                     data = file_data[env] or {}
@@ -131,8 +135,12 @@ class BaseLoader:
                     identifier = f"{self.identifier}_{env}"
                 else:
                     identifier = self.identifier
-
-                self._set_data_to_obj(data, identifier, file_merge, key)
+                self._set_data_to_obj(
+                    data,
+                    identifier,
+                    file_merge,
+                    key,
+                )
 
     def _set_data_to_obj(
         self,
@@ -142,15 +150,16 @@ class BaseLoader:
         key=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)
 
+        if self.obj.filter_strategy:
+            data = self.obj.filter_strategy(data)
+
         # 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,
diff --git a/dynaconf/loaders/ini_loader.py b/dynaconf/loaders/ini_loader.py
index b4c49b0..7b83042 100644
--- a/dynaconf/loaders/ini_loader.py
+++ b/dynaconf/loaders/ini_loader.py
@@ -35,7 +35,11 @@ def load(obj, env=None, silent=True, key=None, filename=None):
         file_reader=lambda fileobj: ConfigObj(fileobj).dict(),
         string_reader=lambda strobj: ConfigObj(strobj.split("\n")).dict(),
     )
-    loader.load(filename=filename, key=key, silent=silent)
+    loader.load(
+        filename=filename,
+        key=key,
+        silent=silent,
+    )
 
 
 def write(settings_path, settings_data, merge=True):
diff --git a/dynaconf/loaders/json_loader.py b/dynaconf/loaders/json_loader.py
index 6c295a4..bfc81aa 100644
--- a/dynaconf/loaders/json_loader.py
+++ b/dynaconf/loaders/json_loader.py
@@ -42,7 +42,11 @@ def load(obj, env=None, silent=True, key=None, filename=None):
         file_reader=file_reader,
         string_reader=string_reader,
     )
-    loader.load(filename=filename, key=key, silent=silent)
+    loader.load(
+        filename=filename,
+        key=key,
+        silent=silent,
+    )
 
 
 def write(settings_path, settings_data, merge=True):
diff --git a/dynaconf/loaders/toml_loader.py b/dynaconf/loaders/toml_loader.py
index 07b973f..8248957 100644
--- a/dynaconf/loaders/toml_loader.py
+++ b/dynaconf/loaders/toml_loader.py
@@ -28,7 +28,11 @@ def load(obj, env=None, silent=True, key=None, filename=None):
         file_reader=toml.load,
         string_reader=toml.loads,
     )
-    loader.load(filename=filename, key=key, silent=silent)
+    loader.load(
+        filename=filename,
+        key=key,
+        silent=silent,
+    )
 
 
 def write(settings_path, settings_data, merge=True):
diff --git a/dynaconf/loaders/yaml_loader.py b/dynaconf/loaders/yaml_loader.py
index 33c6532..fa63150 100644
--- a/dynaconf/loaders/yaml_loader.py
+++ b/dynaconf/loaders/yaml_loader.py
@@ -49,7 +49,11 @@ def load(obj, env=None, silent=True, key=None, filename=None):
         file_reader=yaml_reader,
         string_reader=yaml_reader,
     )
-    loader.load(filename=filename, key=key, silent=silent)
+    loader.load(
+        filename=filename,
+        key=key,
+        silent=silent,
+    )
 
 
 def write(settings_path, settings_data, merge=True):
diff --git a/dynaconf/strategies/__init__.py b/dynaconf/strategies/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/dynaconf/strategies/filtering.py b/dynaconf/strategies/filtering.py
new file mode 100644
index 0000000..47147cc
--- /dev/null
+++ b/dynaconf/strategies/filtering.py
@@ -0,0 +1,19 @@
+from dynaconf.utils import upperfy
+
+
+class PrefixFilter:
+
+    def __init__(self, prefix):
+        if not isinstance(prefix, str):
+            raise TypeError("`SETTINGS_FILE_PREFIX` must be str")
+        self.prefix = "{}_".format(upperfy(prefix))
+
+    def __call__(self, data):
+        """Filter incoming data by prefix"""
+        prefix = self.prefix
+        return {
+                upperfy(
+                    k[len(prefix):]
+                ): v for k, v in data.items()
+                if upperfy(k[:len(prefix)]) == prefix
+         }
diff --git a/example/toml_example/app.py b/example/toml_example/app.py
index 9d1bc9c..a3d14ed 100644
--- a/example/toml_example/app.py
+++ b/example/toml_example/app.py
@@ -1,4 +1,5 @@
-from dynaconf import settings
+from dynaconf import settings, Dynaconf
+from dynaconf.strategies.filtering import PrefixFilter
 
 # print all values in the file
 # using [default] + [development] + [global] values
@@ -51,6 +52,7 @@ assertions = {
     "ENABLED": True,
     "WORKS": "toml_example in dev env",
     "CUSTOM": "this is custom from [development]",
+    "PREFIX_CUSTOM": "this is custom when we set a prefix",
     "TEST_LOADERS": {"dev": "test_dev", "prod": "test_prod"},
 }
 
@@ -79,3 +81,15 @@ for key, value in assertions.items():
     found = settings.from_env("production").get(key)
     assert found == getattr(settings.from_env("production"), key)
     assert found == value, f"expected: {key}: [{value}] found: [{found}]"
+
+
+settings_with_prefix = Dynaconf(
+    settings_files=["settings.toml"],
+    filter_strategy=PrefixFilter("prefix"),
+    environments=True,
+)
+with settings_with_prefix.using_env("default"):
+    assert settings_with_prefix.CUSTOM == "this is custom when we set a prefix"
+assert settings_with_prefix.from_env(
+    "production"
+).CUSTOM == "this is custom when we set a prefix"
diff --git a/example/toml_example/settings.toml b/example/toml_example/settings.toml
index 478ada0..5a85408 100644
--- a/example/toml_example/settings.toml
+++ b/example/toml_example/settings.toml
@@ -13,6 +13,7 @@ age = 42.0
 enabled = true
 works = "toml_example"
 custom = 'this is custom from [default]'
+prefix_custom = "this is custom when we set a prefix"
 
   [default.test_loaders]
   dev = "test_dev"
diff --git a/example/yaml_example/settings_file/app.py b/example/yaml_example/settings_file/app.py
index 4a701e6..bc1eb4d 100644
--- a/example/yaml_example/settings_file/app.py
+++ b/example/yaml_example/settings_file/app.py
@@ -1,4 +1,5 @@
-from dynaconf import settings
+from dynaconf import settings, LazySettings
+from dynaconf.strategies.filtering import PrefixFilter
 
 # print all values in the file
 # using [default] + [development] + [global] values
@@ -51,6 +52,7 @@ assertions = {
     "ENABLED": True,
     "WORKS": "yaml_example in dev env",
     "CUSTOM": "this is custom from [development]",
+    "PREFIX_CUSTOM": "this is custom when prefix is set",
     "TEST_LOADERS": {"dev": "test_dev", "prod": "test_prod"},
 }
 
@@ -71,6 +73,7 @@ assertions = {
     "ENABLED": True,
     "WORKS": "yaml_example in prod env",
     "CUSTOM": "this is custom from [production]",
+    "PREFIX_CUSTOM": "this is custom when prefix is set",
     "TEST_LOADERS": {"dev": "test_dev", "prod": "test_prod"},
 }
 
@@ -79,3 +82,14 @@ for key, value in assertions.items():
     found = settings.from_env("production").get(key)
     assert found == getattr(settings.from_env("production"), key)
     assert found == value, f"expected: {key}: [{value}] found: [{found}]"
+
+settings = LazySettings(
+    settings_files=["settings.yaml"],
+    filter_strategy=PrefixFilter("prefix"),
+    environments=True,
+)
+with settings.using_env("default"):
+    assert settings.CUSTOM == "this is custom when prefix is set"
+assert settings.from_env(
+    "production"
+).CUSTOM == "this is custom when prefix is set"
diff --git a/example/yaml_example/settings_file/settings.yaml b/example/yaml_example/settings_file/settings.yaml
index 15fae7b..ea5ac0b 100644
--- a/example/yaml_example/settings_file/settings.yaml
+++ b/example/yaml_example/settings_file/settings.yaml
@@ -12,6 +12,7 @@ default:
   enabled: true
   works: yaml_example
   custom: 'this is custom from [default]'
+  prefix_custom: "this is custom when prefix is set"
   test_loaders:
     dev: test_dev
     prod: test_prod


Test patch:
diff --git a/tests/test_base.py b/tests/test_base.py
index b750b83..adfb883 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -3,6 +3,7 @@ import os
 import pytest
 
 from dynaconf import Dynaconf
+from dynaconf.strategies.filtering import PrefixFilter
 from dynaconf import LazySettings
 from dynaconf.loaders import toml_loader
 from dynaconf.utils.parse_conf import true_values
@@ -754,6 +755,45 @@ def test_from_env_method(clean_env, tmpdir):
     assert settings.A_DEFAULT == "From default env"
 
 
+def test_from_env_method_with_prefix(clean_env, tmpdir):
+    data = {
+        "default": {"prefix_a_default": "From default env"},
+        "development": {
+            "prefix_value": "From development env",
+            "prefix_only_in_development": True,
+        },
+        "other": {"prefix_value": "From other env", "prefix_only_in_other": True, "not_prefixed": "no prefix"},
+    }
+    toml_path = str(tmpdir.join("base_settings.toml"))
+    toml_loader.write(toml_path, data, merge=False)
+    settings = LazySettings(settings_file=toml_path, environments=True, filter_strategy=PrefixFilter("prefix"))
+    settings.set("ARBITRARY_KEY", "arbitrary value")
+
+    assert settings.VALUE == "From development env"
+    assert settings.A_DEFAULT == "From default env"
+    assert settings.ONLY_IN_DEVELOPMENT is True
+    assert settings.ARBITRARY_KEY == "arbitrary value"
+    assert settings.get("ONLY_IN_OTHER") is None
+
+    # clone the settings object pointing to a new env
+    other_settings = settings.from_env("other")
+    assert other_settings.VALUE == "From other env"
+    assert other_settings.A_DEFAULT == "From default env"
+    assert other_settings.ONLY_IN_OTHER is True
+    assert other_settings.get("ARBITRARY_KEY") is None
+    assert other_settings.get("ONLY_IN_DEVELOPMENT") is None
+    with pytest.raises(AttributeError):
+        other_settings.not_prefixed
+    with pytest.raises(AttributeError):
+        # values set programatically are not cloned
+        other_settings.ARBITRARY_KEY
+    with pytest.raises(AttributeError):
+        # values set only in a specific env not cloned
+        other_settings.ONLY_IN_DEVELOPMENT
+    # assert it is cached not created twice
+    assert other_settings is settings.from_env("other")
+
+
 def test_preload(tmpdir):
     data = {
         "data": {"links": {"twitter": "rochacbruno", "site": "brunorocha.org"}}
@@ -842,6 +882,26 @@ def test_envless_mode(tmpdir):
     assert settings.DATABASES.default.port == 8080
 
 
+def test_envless_mode_with_prefix(tmpdir):
+    data = {
+        "prefix_foo": "bar",
+        "hello": "world",
+        "prefix_default": 1,
+        "prefix_databases": {"default": {"port": 8080}},
+    }
+    toml_loader.write(str(tmpdir.join("settings.toml")), data)
+
+    settings = LazySettings(
+        settings_file="settings.toml",
+        filter_strategy=PrefixFilter("prefix")
+    )  # already the default
+    assert settings.FOO == "bar"
+    with pytest.raises(AttributeError):
+        settings.HELLO
+    assert settings.DEFAULT == 1
+    assert settings.DATABASES.default.port == 8080
+
+
 def test_lowercase_read_mode(tmpdir):
     """
     Starting on 3.0.0 lowercase keys are enabled by default
@@ -897,3 +957,10 @@ def test_settings_dict_like_iteration(tmpdir):
 
     for key, value in settings.items():
         assert settings._store[key] == value
+
+
+def test_prefix_is_not_str_raises():
+    with pytest.raises(TypeError):
+        toml_loader.load(LazySettings(filter_strategy=PrefixFilter(int)))
+    with pytest.raises(TypeError):
+        toml_loader.load(LazySettings(filter_strategy=PrefixFilter(True)))
diff --git a/tests/test_ini_loader.py b/tests/test_ini_loader.py
index 48f9118..2575e04 100644
--- a/tests/test_ini_loader.py
+++ b/tests/test_ini_loader.py
@@ -1,6 +1,7 @@
 import pytest
 
 from dynaconf import LazySettings
+from dynaconf.strategies.filtering import PrefixFilter
 from dynaconf.loaders.ini_loader import load
 
 settings = LazySettings(environments=True, ENV_FOR_DYNACONF="PRODUCTION")
@@ -189,3 +190,17 @@ def test_envless():
     assert settings.a == "a,b"
     assert settings.COLORS.white.code == "#FFFFFF"
     assert settings.COLORS.white.name == "white"
+
+
+def test_prefix():
+    settings = LazySettings(filter_strategy=PrefixFilter("prefix"))
+    ini = """
+    prefix_a = "a,b"
+    prefix_colors__white__code = '#FFFFFF'
+    COLORS__white__name = 'white'
+    """
+    load(settings, filename=ini)
+    assert settings.a == "a,b"
+    assert settings.COLORS.white.code == "#FFFFFF"
+    with pytest.raises(AttributeError):
+        settings.COLORS.white.name
diff --git a/tests/test_json_loader.py b/tests/test_json_loader.py
index 45c32ca..63e94e5 100644
--- a/tests/test_json_loader.py
+++ b/tests/test_json_loader.py
@@ -3,6 +3,7 @@ import json
 import pytest
 
 from dynaconf import LazySettings
+from dynaconf.strategies.filtering import PrefixFilter
 from dynaconf.loaders.json_loader import DynaconfEncoder
 from dynaconf.loaders.json_loader import load
 
@@ -212,3 +213,17 @@ def test_envless():
     load(settings, filename=_json)
     assert settings.COLORS.yellow.code == "#FFCC00"
     assert settings.COLORS.yellow.name == "Yellow"
+
+
+def test_prefix():
+    settings = LazySettings(filter_strategy=PrefixFilter("prefix"))
+    _json = """
+    {
+        "prefix_colors__yellow__code": "#FFCC00",
+        "COLORS__yellow__name": "Yellow"
+    }
+    """
+    load(settings, filename=_json)
+    assert settings.COLORS.yellow.code == "#FFCC00"
+    with pytest.raises(AttributeError):
+        settings.COLORS.yellow.name
diff --git a/tests/test_toml_loader.py b/tests/test_toml_loader.py
index 4e8eb7a..a28fde5 100644
--- a/tests/test_toml_loader.py
+++ b/tests/test_toml_loader.py
@@ -1,6 +1,7 @@
 import pytest
 
 from dynaconf import LazySettings
+from dynaconf.strategies.filtering import PrefixFilter
 from dynaconf.loaders.toml_loader import encode_nulls
 from dynaconf.loaders.toml_loader import load
 
@@ -206,3 +207,17 @@ def test_envless():
     assert settings.a == "a,b"
     assert settings.COLORS.white.code == "#FFFFFF"
     assert settings.COLORS.white.name == "white"
+
+
+def test_prefix():
+    settings = LazySettings(filter_strategy=PrefixFilter("prefix"))
+    ini = """
+    prefix_a = "a,b"
+    prefix_colors__white__code = '#FFFFFF'
+    COLORS__white__name = 'white'
+    """
+    load(settings, filename=ini)
+    assert settings.a == "a,b"
+    assert settings.COLORS.white.code == "#FFFFFF"
+    with pytest.raises(AttributeError):
+        settings.COLORS.white.name
diff --git a/tests/test_yaml_loader.py b/tests/test_yaml_loader.py
index 1158934..e6fe382 100644
--- a/tests/test_yaml_loader.py
+++ b/tests/test_yaml_loader.py
@@ -3,6 +3,7 @@ import os
 import pytest
 
 from dynaconf import LazySettings
+from dynaconf.strategies.filtering import PrefixFilter
 from dynaconf.loaders.yaml_loader import load
 
 settings = LazySettings(
@@ -297,6 +298,20 @@ def test_envless():
     assert settings.COLORS.white.name == "white"
 
 
+def test_prefix():
+    settings = LazySettings(filter_strategy=PrefixFilter("prefix"))
+    _yaml = """
+    prefix_a: a,b
+    prefix_colors__white__code: "#FFFFFF"
+    COLORS__white__name: "white"
+    """
+    load(settings, filename=_yaml)
+    assert settings.a == "a,b"
+    assert settings.COLORS.white.code == "#FFFFFF"
+    with pytest.raises(AttributeError):
+        settings.COLORS.white.name
+
+
 def test_empty_env():
     settings = LazySettings(environments=True)
     _yaml = """