Repository: rochacbruno/dynaconf
Pull request #91 (2018-10-08 12:38:33)

Update django.md
Typo!

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

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

```diff
@@          Coverage Diff          @@
##           master    #91   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          17     17           
  Lines         568    568           
=====================================
  Hits          568    568
```



------

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


Patch:
diff --git a/.travis.yml b/.travis.yml
index 5fe0738..e6cb873 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -57,28 +57,31 @@ jobs:
         - "pip install --upgrade -r requirements_dev.txt"
       script: make test
 
-    # PYYAML IS BROKEN in 3.7 dev
-    # - stage: Python 3.7
-    #   language: python
-    #   python: "3.7-dev"
-    #   env:
-    #     - DYNACONF_SETTINGS=dynaconf.test_settings
-    #     - DOTENV_PATH_FOR_DYNACONF=/home/travis/build/rochacbruno/dynaconf/.env
-    #     - DYNATRAVIS_ENV_BOOLEAN='@bool true'
-    #     - DYNATRAVIS_ENV_INT='@int 42'
-    #     - DYNATRAVIS_ENV_FLOAT='@float 42.2'
-    #     - DYNATRAVIS_ENV_LIST='@json ["dyna", "conf"]'
-    #     # - DYNATRAVIS_ENV_DICT='@json {"dyna": "conf"}' how to fix this?
-    #     - DYNATRAVIS_ENV_DICT='@json {}'
-    #     - DYNATRAVIS_ENV_PURE_INT=42
-    #     - DYNATRAVIS_ENV_STR_INT="'42'"
-    #     - OTHER_TESTING='@bool yes'
-    #     - OTHER_ENABLED=true
-    #     - OTHER_DISABLED=false
-    #   install:
-    #     - "pip install --upgrade pip"
-    #     - "pip install --upgrade -r requirements_dev.txt"
-    #   script: make test
+    - stage: Python 3.7
+      language: python
+      python: "3.7"
+      dist: xenial
+      sudo: yes
+      env:
+        - DYNACONF_SETTINGS=dynaconf.test_settings
+        - DOTENV_PATH_FOR_DYNACONF=/home/travis/build/rochacbruno/dynaconf/.env
+        - DYNATRAVIS_ENV_BOOLEAN='@bool true'
+        - DYNATRAVIS_ENV_INT='@int 42'
+        - DYNATRAVIS_ENV_FLOAT='@float 42.2'
+        - DYNATRAVIS_ENV_LIST='@json ["dyna", "conf"]'
+        # - DYNATRAVIS_ENV_DICT='@json {"dyna": "conf"}' how to fix this?
+        - DYNATRAVIS_ENV_DICT='@json {}'
+        - DYNATRAVIS_ENV_PURE_INT=42
+        - DYNATRAVIS_ENV_STR_INT="'42'"
+        - OTHER_TESTING='@bool yes'
+        - OTHER_ENABLED=true
+        - OTHER_DISABLED=false
+      before_script:
+        - sleep 5
+      install:
+        - "pip install --upgrade pip"
+        - "pip install --upgrade -r requirements_dev.txt"
+      script: make test
 
     - stage: Python 3.6 with coverage
       language: python
diff --git a/HISTORY b/HISTORY
index 7e161ab..fd979d9 100644
--- a/HISTORY
+++ b/HISTORY
@@ -3,9 +3,24 @@ Changelog
 
 These are all the changes in Dynaconf since the first public release.
 
-1.0.7 - Unreleased
+1.1.1
 -----
 
+- Unreleased
+- Added `INSTANCE_FOR_DYNACONF` and `--instance` parameters. These parameters can tell cli tools to use custom LazySettings instance. Fixes #108.
+- Fixed `dynaconf validate` bug, when `--path` value does not end with `.toml` exception was rised.
+- Fixed `dynaconf validate` validation error handling, now instead of raising ValidationError an error message is printed and if there are errors "Validateion success!" is not printed.
+
+
+1.1.0
+-----
+
+- Added `MERGE_ENABLED_FOR_DYNACONF` with ability to merge nested dictionaries instead of replacing PR #88
+- Support for dot notation to access nested dictionaries like `settings['KEY.OTHER.NESTED.DEEPER']` PR #93
+- Support dotted notation for validators PR #98
+- Fixed a bug in SETTINGS_MODULE cleanup when `.setenv` method was called PR #97
+- Added Python 3.7 to test matrix PR #99
+
 1.0.6
 -----
 
@@ -20,20 +35,20 @@ These are all the changes in Dynaconf since the first public release.
 1.0.4
 -----
 
-- Fix Django definition of GLOBAL_ENV 
+- Fix Django definition of GLOBAL_ENV
 - Fix in __getitem__ method
 
 1.0.3
 -----
 
-- Excluded example and tests from realease dist 
+- Excluded example and tests from realease dist
 - removed root logger configuration
 
 1.0.2
 -----
 - Improved docs
 - Fix error when some loader is not installed
-- Fix loader import error 
+- Fix loader import error
 - Improved logs
 
 1.0.1
@@ -44,9 +59,9 @@ These are all the changes in Dynaconf since the first public release.
 
 1.0.0
 -----
-- Breaking changes 
+- Breaking changes
 - `namespace` renamed to `env`
-- added concept of DEFAULT and GLOBAL envs 
+- added concept of DEFAULT and GLOBAL envs
 - `env` will default to DEVELOPMENT until changed by envvar `ENV_FOR_DYNACONF`
 
 0.7.6
@@ -87,13 +102,13 @@ These are all the changes in Dynaconf since the first public release.
 
 0.6.0
 -----
-- Drop Python 2.x from Travis CI (must work but will not be tested) 
+- Drop Python 2.x from Travis CI (must work but will not be tested)
 - Fix Flask requirement #41
 
 0.5.6
 -----
-- Added support for .ini and .json files 
-- parse conf is now recursive to parse dict inner data 
+- Added support for .ini and .json files
+- parse conf is now recursive to parse dict inner data
 - Cloned find_file function from dotenv
 
 0.5.5
diff --git a/Makefile b/Makefile
index 7f4609b..438fc02 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,9 @@ test_examples:
 	@cd example/;pwd;python compat.py
 	@cd example/app;pwd;python app.py | grep -c app
 	@cd example/app_with_dotenv;pwd;python app.py | grep -c app_with_dotenv
+	@cd example/merge_configs;pwd;python app.py | grep -c merge_configs
 	@cd example/multiple_sources;pwd;python app.py | grep -c multiple_sources
+	@cd example/multiple_folders;pwd;python app.py | grep -c var2_prod
 	@cd example/toml_example/;pwd;python app.py | grep -c toml_example
 	@cd example/yaml_example/settings_module/;pwd;python app.py | grep -c yaml_example
 	@cd example/yaml_example/yaml_as_extra_config/;pwd;python app.py | grep -c yaml_as_extra_config
@@ -17,6 +19,7 @@ test_examples:
 	@cd example/flask_with_json;pwd;flask routes | grep -c flask_with_json
 	@cd example/flask_with_ini;pwd;flask routes | grep -c flask_with_ini
 	@cd example/validators/with_python/;pwd;python app.py | grep -c validator
+	@cd example/validators/with_toml/;pwd;dynaconf validate
 	@cd example/toml_with_secrets/;pwd;python program.py | grep -c My5up3r53c4et
 	@cd example/envs;pwd;python app.py
 	@cd example/django_example/;pwd;python manage.py test
@@ -42,7 +45,11 @@ install:
 	python setup.py develop
 
 pep8:
-	@flake8 dynaconf --ignore=F403
+	# Flake8 ignores 
+	#   F841 (local variable assigned but never used, useful for debugging on exception)
+	#   W504 (line break after binary operator, I prefer to put `and|or` at the end)
+	#   F403 (star import `from foo import *` often used in __init__ files)
+	@flake8 dynaconf --ignore=F403,W504,F841
 
 release: clean test_examples test
 	@python setup.py sdist bdist_wheel
diff --git a/README.md b/README.md
index 9c4ae18..b07d112 100644
--- a/README.md
+++ b/README.md
@@ -110,7 +110,7 @@ ENV_FOR_DYNACONF=staging python yourapp.py
 
 ## The settings files
 
-> **NOTE:** [Read the full documentaion](http://dynaconf.readthedocs.io/en/latest/index.html) about dynaconf CLI to learn how to automatically create the settings files for your project.
+> **NOTE:** [Read the full documentation](http://dynaconf.readthedocs.io/en/latest/index.html) about dynaconf CLI to learn how to automatically create the settings files for your project.
 
 An optional `settings.{toml|py|json|ini|yaml}` file can be used to specify the configuration parameters for each environment. If it is not present, only the values from **environment variables** are used (**.env** file is also supported). Dynaconf searches for the file starting at the current working directory. If it is not found there, Dynaconf checks the parent directory. Dynaconf continues checking parent directories until the root is reached.
 
@@ -174,7 +174,7 @@ pip3 install dynaconf[yaml|ini|redis|vault]
 pip3 install dynaconf[all]
 ```
 
-Once the support is installed no extra configuration is needed to load data from those files, dynaconf will search for settings files in
+Once the support is installed, no extra configuration is needed to load data from those files. Dynaconf will search for settings files in
 the root directory of your application looking for the following files in the exact order below:
 
 ```python
diff --git a/docs/guides/accessing_values.md b/docs/guides/accessing_values.md
index a553cc8..59396bc 100644
--- a/docs/guides/accessing_values.md
+++ b/docs/guides/accessing_values.md
@@ -41,6 +41,20 @@ settings.get('TIMEOUT', 300)
 
 > Returns the default (300) if not defined
 
+Using dotted-path lookup
+
+```python
+settings.get('AUTH.USER', 'anonymous')
+```
+
+> Returns the default ('anonymous') if not defined
+
+Explicitly disabling dotted-path lookup
+
+```python
+settings.get('AUTH.USER', dotted_lookup=False)
+```
+
 ## Forcing type casting
 
 ```python
diff --git a/docs/guides/cli.md b/docs/guides/cli.md
index f57f359..b579ca4 100644
--- a/docs/guides/cli.md
+++ b/docs/guides/cli.md
@@ -15,6 +15,7 @@ Options:
   --version  Show dynaconf version
   --docs     Open documentation in browser
   --banner   Show awesome banner
+  -i, --instance TEXT  Custom instance of LazySettings
   --help     Show this message and exit.
 
 Commands:
diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md
index ae281e4..a1ed4d7 100644
--- a/docs/guides/configuration.md
+++ b/docs/guides/configuration.md
@@ -29,6 +29,8 @@ Dynaconf can be configured through variables suffixed with `_FOR_DYNACONF` those
 +-----------------+---------+--------------------------------------------------+-------------------------+--------------------------------------------------------------+
 | AUTO_CAST       | bool    | `@casting` like `@int` is parsed                 | true                    | DOTENV_OVERRIDE_FOR_DYNACONF=false                           |
 +-----------------+---------+--------------------------------------------------+-------------------------+--------------------------------------------------------------+
+| MERGE_ENABLED   | bool    | Merge vars instead of overwriting them           | false                   | MERGE_ENABLED_FOR_DYNACONF=true                              |
++-----------------+---------+--------------------------------------------------+-------------------------+--------------------------------------------------------------+
 | REDIS_ENABLED   | bool    | Redis loader is enabled                          | false                   | REDIS_ENABLED_FOR_DYNACONF=true                              |
 +-----------------+---------+--------------------------------------------------+-------------------------+--------------------------------------------------------------+
 | VAULT_ENABLED   | bool    | Vault server is enabled                          | false                   | VAULT_ENABLED_FOR_DYNACONF=true                              |
@@ -47,6 +49,8 @@ Dynaconf can be configured through variables suffixed with `_FOR_DYNACONF` those
 +-----------------+---------+--------------------------------------------------+-------------------------+--------------------------------------------------------------+
 | VAULT_VERIFY    | bool    | vault should verify                              | None                    | VAULT_VERIFY_FOR_DYNACONF=true                               |
 +-----------------+---------+--------------------------------------------------+-------------------------+--------------------------------------------------------------+
+| INSTANCE        | str     | custom instance of LazySettings                  | None                    | INSTANCE_FOR_DYNACONF=myapp.settings                         |
++-----------------+---------+--------------------------------------------------+-------------------------+--------------------------------------------------------------+
 
 ```
 
diff --git a/docs/guides/django.md b/docs/guides/django.md
index 5db3ac8..1dfc022 100644
--- a/docs/guides/django.md
+++ b/docs/guides/django.md
@@ -10,7 +10,7 @@ this extension makes your Django's `conf.settings` in to a `dynaconf` instance.
 In your django project's `settings.py` include:
 
 ```python
-# *Django `check` requires this before loading isntalled apps, this can be overwriten by dynaconf later
+# *Django `check` requires this before loading installed apps, this can be overwriten by dynaconf later
 SECRET_KEY = True  
 
 # Load dynaconf
diff --git a/docs/guides/validation.md b/docs/guides/validation.md
index d21b23c..f17beef 100644
--- a/docs/guides/validation.md
+++ b/docs/guides/validation.md
@@ -43,6 +43,24 @@ settings.validators.validate()
 
 The above will raise `dynaconf.validators.ValidationError("AGE must be lte=30 but it is 35 in env DEVELOPMENT")` and `dynaconf.validators.ValidationError("PROJECT must be eq='hello_world' but it is 'This is not hello_world' in env PRODUCTION")`
 
+You can also use dot-delimited paths for registering validators on nested structures:
+
+```python
+from dynaconf import settings, Validator
+
+# Register validators
+settings.validators.register(
+
+    # Ensure the database.host field exists.
+    Validator('DATABASE.HOST', must_exist=True),
+
+    # Make the database.password field optional.
+    Validator('DATABASE.PASSWORD', must_exist=True),
+
+# Fire the validator
+settings.validators.validate()
+```
+
 ## CLI and dynaconf_validators.toml
 
 > **NEW in 1.0.1**
@@ -58,6 +76,9 @@ version = {must_exist=true}
 name = {must_exist=true}
 password = {must_exist=false}
 
+# dot notation is also supported
+'a_big_dict.nested_1.nested_2.nested_3.nested_4' = {must_exist=true, eq=1}
+
   [default.age]
   must_exist = true
   lte = 30
diff --git a/dynaconf/VERSION b/dynaconf/VERSION
index 238d6e8..524cb55 100644
--- a/dynaconf/VERSION
+++ b/dynaconf/VERSION
@@ -1,1 +1,1 @@
-1.0.7
+1.1.1
diff --git a/dynaconf/base.py b/dynaconf/base.py
index 3eba1b8..fcdf644 100644
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -1,7 +1,9 @@
 # coding: utf-8
 import os
+import glob
 import importlib
 from contextlib import contextmanager
+from collections import OrderedDict
 
 from six import string_types
 
@@ -14,7 +16,13 @@ from dynaconf.loaders import (
 )
 from dynaconf.utils.functional import LazyObject, empty
 from dynaconf.utils.parse_conf import converters, parse_conf_data, true_values
-from dynaconf.utils import BANNER, compat_kwargs, raw_logger
+from dynaconf.utils import (
+    BANNER,
+    compat_kwargs,
+    raw_logger,
+    object_merge,
+    missing
+)
 from dynaconf.validator import ValidatorList
 from dynaconf.utils.boxing import DynaBox
 
@@ -77,8 +85,6 @@ class LazySettings(LazyObject):
         """
         compat_kwargs(kwargs)
         self._kwargs = kwargs
-        for k, v in kwargs.items():
-            setattr(default_settings, k.upper(), v)
         super(LazySettings, self).__init__()
 
     def __getattr__(self, name):
@@ -107,7 +113,8 @@ class LazySettings(LazyObject):
 
     def _setup(self):
         """Initial setup, run once."""
-        environment_variable = default_settings.ENVVAR_FOR_DYNACONF
+        environment_variable = self._kwargs.get(
+            'ENVVAR_FOR_DYNACONF', default_settings.ENVVAR_FOR_DYNACONF)
         settings_module = os.environ.get(environment_variable)
         self._wrapped = Settings(
             settings_module=settings_module, **self._kwargs
@@ -155,6 +162,7 @@ class Settings(object):
         self.environ = os.environ
         self.SETTINGS_MODULE = None
         self._not_installed_warnings = []
+        self._memoized = None
 
         compat_kwargs(kwargs)
         if settings_module:
@@ -210,7 +218,26 @@ class Settings(object):
         """Redirects to store object"""
         return self.store.values()
 
-    def get(self, key, default=None, cast=None, fresh=False):
+    def _dotted_get(self, dotted_key, default=None, **kwargs):
+        """
+        Perform dotted key lookups and keep track of where we are.
+        """
+        split_key = dotted_key.split('.')
+        name, keys = split_key[0], split_key[1:]
+        result = self.get(name, default=default, **kwargs)
+        self._memoized = result
+
+        # If we've reached the end, then return result and clear the
+        # memoized data.
+        if not keys or result is default:
+            self._memoized = None
+            return result
+
+        # If we've still got key elements to traverse, let's do that.
+        return self._dotted_get(".".join(keys), default=default, **kwargs)
+
+    def get(self, key, default=None, cast=None, fresh=False,
+            dotted_lookup=True):
         """
         Get a value from settings store, this is the prefered way to access::
 
@@ -221,9 +248,15 @@ class Settings(object):
         :param default: In case of not found it will be returned
         :param cast: Should cast in to @int, @float, @bool or @json ?
         :param fresh: Should reload from loaders store before access?
+        :param dotted_lookup: Should perform dotted-path lookup?
         :return: The value if found, default or None
         """
         key = key.upper()
+
+        if '.' in key and dotted_lookup:
+            return self._dotted_get(dotted_key=key, default=default, cast=cast,
+                                    fresh=fresh)
+
         if key in self._deleted:
             return default
 
@@ -233,7 +266,9 @@ class Settings(object):
         ):
             self.unset(key)
             self.execute_loaders(key=key)
-        data = self.store.get(key, default)
+
+        store = self._memoized or self.store
+        data = store.get(key, default)
         if cast:
             data = converters.get(cast)(data)
         return data
@@ -248,12 +283,7 @@ class Settings(object):
         key = key.upper()
         if key in self._deleted:
             return False
-        if (
-            (fresh or self._fresh or key in self.FRESH_VARS_FOR_DYNACONF) and
-            key not in dir(default_settings)
-        ):
-            self.execute_loaders(key=key)
-        return key in self.store
+        return self.get(key, fresh=fresh, default=missing) is not missing
 
     def get_fresh(self, key, default=None, cast=None):
         """This is a shortcut to `get(key, fresh=True)`. always reload from
@@ -410,7 +440,7 @@ class Settings(object):
             self.ENVVAR_FOR_DYNACONF,
             self.SETTINGS_MODULE_FOR_DYNACONF
         )
-        if settings_module != self.SETTINGS_MODULE:
+        if settings_module != getattr(self, 'SETTINGS_MODULE', None):
             self.set('SETTINGS_MODULE', settings_module)
         return self.SETTINGS_MODULE
 
@@ -485,7 +515,22 @@ class Settings(object):
         for key in keys:
             self.unset(key)
 
-    def set(self, key, value, loader_identifier=None, tomlfy=False):
+    def _dotted_set(self, dotted_key, value, tomlfy=False, **kwargs):
+
+        data = DynaBox(default_box=True)
+        tree = data
+        split_keys = dotted_key.split('.')
+
+        for k in split_keys[:-1]:
+            tree = tree.setdefault(k, {})
+
+        value = parse_conf_data(value, tomlfy=tomlfy)
+        tree[split_keys[-1]] = value
+
+        self.update(data=data, **kwargs)
+
+    def set(self, key, value, loader_identifier=None, tomlfy=False,
+            dotted_lookup=True):
         """Set a value storing references for the loader
 
         :param key: The key to store
@@ -493,11 +538,24 @@ class Settings(object):
         :param loader_identifier: Optional loader name e.g: toml, yaml etc.
         :param tomlfy: Bool define if value is parsed by toml (defaults False)
         """
+
+        if '.' in key and dotted_lookup is True:
+            return self._dotted_set(
+                key,
+                value,
+                loader_identifier=loader_identifier,
+                tomlfy=tomlfy)
+
         value = parse_conf_data(value, tomlfy=tomlfy)
+        key = key.strip().upper()
+
+        if getattr(self, 'MERGE_ENABLED_FOR_DYNACONF', False):
+            attr = getattr(self, key, None)
+            object_merge(attr, value)
+
         if isinstance(value, dict):
             value = DynaBox(value, box_it_up=True)
 
-        key = key.strip().upper()
         setattr(self, key, value)
         self.store[key] = value
         self._deleted.discard(key)
@@ -576,6 +634,35 @@ class Settings(object):
             self.logger.debug('Dynaconf executing: %s', loader.__name__)
             loader.load(self, env, silent=silent, key=key)
 
+        # Do we have any nested includes we need to process? If so,
+        # Remove any duplicate paths (while preserving definition order)
+        # by converting to an ordered dict, and then converting back
+        # to a list.
+        includes = list(OrderedDict.fromkeys(self.get('DYNACONF_INCLUDE', [])))
+        if includes:
+            already_loaded = set()
+            basepath = os.path.split(self.settings_module)[0]
+
+            for include in includes:
+                # Handle absolute and relative paths in the configuration.
+                if os.path.isabs(include):  # pragma: no cover
+                    included_filename = include
+                else:
+                    included_filename = os.path.join(basepath, include)
+
+                # Handle possible globs
+                for path in glob.glob(included_filename):
+                    if path in already_loaded:  # pragma: no cover
+                        continue
+
+                    settings_loader(obj=self,
+                                    env=env,
+                                    silent=silent,
+                                    key=key,
+                                    filename=path)
+
+                    already_loaded.add(path)
+
     def load_extra_yaml(self, env, silent, key):
         """This is deprecated, kept for compat
 
diff --git a/dynaconf/cli.py b/dynaconf/cli.py
index 163743d..a5a46c5 100644
--- a/dynaconf/cli.py
+++ b/dynaconf/cli.py
@@ -9,44 +9,75 @@ import webbrowser
 from pathlib import Path
 from dynaconf import default_settings
 from dynaconf import constants
-from dynaconf.validator import Validator
+from dynaconf import LazySettings
+from dynaconf.validator import Validator, ValidationError
 from dynaconf.utils.parse_conf import parse_conf_data
 from dotenv import cli as dotenv_cli
 from contextlib import suppress
 
+CWD = Path.cwd()
+ENVS = ['default', 'development', 'staging', 'testing', 'production', 'global']
+EXTS = ['ini', 'toml', 'yaml', 'json', 'py', 'env']
+WRITERS = ['ini', 'toml', 'yaml', 'json', 'py', 'redis', 'vault', 'env']
 
-flask_app = None
-django_app = None
+ENC = default_settings.ENCODING_FOR_DYNACONF
 
-if 'FLASK_APP' in os.environ:  # pragma: no cover
-    with suppress(ImportError, click.UsageError):
-        from flask.cli import ScriptInfo
-        flask_app = ScriptInfo().load_app()
-        settings = flask_app.config
-        click.echo(click.style('Flask app detected', fg='white', bg='black'))
 
+def set_settings(instance=None):
+    """Pick correct settings instance and set it to a global variable."""
 
-if 'DJANGO_SETTINGS_MODULE' in os.environ:  # pragma: no cover
-    sys.path.insert(0, os.path.abspath('.'))
-    with suppress(Exception):
-        import dynaconf.contrib.django_dynaconf  # noqa
-        from django.conf import settings as django_settings
-        django_settings.configure()
-        settings = django_settings
-        django_app = True
-        click.echo(click.style('Django app detected', fg='white', bg='black'))
+    global settings
 
+    settings = None
 
-if not django_app and not flask_app:
-    from dynaconf import settings
+    if instance:
+        settings = import_settings(instance)
 
+    elif 'INSTANCE_FOR_DYNACONF' in os.environ:
+        settings = import_settings(os.environ['INSTANCE_FOR_DYNACONF'])
 
-CWD = Path.cwd()
-ENVS = ['default', 'development', 'staging', 'testing', 'production', 'global']
-EXTS = ['ini', 'toml', 'yaml', 'json', 'py', 'env']
-WRITERS = ['ini', 'toml', 'yaml', 'json', 'py', 'redis', 'vault', 'env']
+    elif 'FLASK_APP' in os.environ:  # pragma: no cover
+        with suppress(ImportError, click.UsageError):
+            from flask.cli import ScriptInfo
+            flask_app = ScriptInfo().load_app()
+            settings = flask_app.config
+            click.echo(click.style(
+                'Flask app detected', fg='white', bg='black'))
+
+    elif 'DJANGO_SETTINGS_MODULE' in os.environ:  # pragma: no cover
+        sys.path.insert(0, os.path.abspath('.'))
+        with suppress(Exception):
+            import dynaconf.contrib.django_dynaconf  # noqa
+            from django.conf import settings as django_settings
+            django_settings.configure()
+            settings = django_settings
+            click.echo(click.style(
+                'Django app detected', fg='white', bg='black'))
+
+    if settings is None:
+        settings = LazySettings()
 
-ENC = default_settings.ENCODING_FOR_DYNACONF
+
+def import_settings(dotted_path):
+    """Import settings instance from python dotted path.
+
+    Last item in dotted path must be settings instace.
+
+    Example: import_settings('path.to.settings')
+    """
+    if '.' in dotted_path:
+        module, name = dotted_path.rsplit('.', 1)
+    else:
+        raise click.UsageError(
+            "invalid path to settings instance: {}".format(dotted_path))
+    try:
+        module = importlib.import_module(module)
+    except ImportError as e:
+        raise click.UsageError(e)
+    try:
+        return getattr(module, name)
+    except AttributeError as e:
+        raise click.UsageError(e)
 
 
 def split_vars(_vars):
@@ -86,6 +117,7 @@ def show_banner(ctx, param, value):
     """Shows dynaconf awesome banner"""
     if not value or ctx.resilient_parsing:
         return
+    set_settings()
     click.echo(settings.dynaconf_banner)
     click.echo('Learn more at: http://github.com/rochacbruno/dynaconf')
     ctx.exit()
@@ -98,10 +130,13 @@ def show_banner(ctx, param, value):
               is_eager=True, help="Open documentation in browser")
 @click.option('--banner', is_flag=True, callback=show_banner,
               expose_value=False, is_eager=True, help="Show awesome banner")
-def main():
+@click.option('--instance', '-i', default=None,
+              help="Custom instance of LazySettings")
+def main(instance):
     """Dynaconf - Command Line Interface\n
     Documentation: http://dynaconf.readthedocs.io/
     """
+    set_settings(instance)
 
 
 @main.command()
@@ -161,7 +196,7 @@ def init(fileformat, path, env, _vars, _secrets, wg, y):
 
     path = Path(path)
 
-    if str(path).endswith(constants.ALL_EXTENSIONS + ('py',)):
+    if str(path).endswith(constants.ALL_EXTENSIONS + ('py',)):  # pragma: no cover  # noqa
         settings_path = path
         secrets_path = path.parent / '.secrets.{}'.format(fileformat)
         dotenv_path = path.parent / '.env'
@@ -170,7 +205,7 @@ def init(fileformat, path, env, _vars, _secrets, wg, y):
         if fileformat == 'env':
             if str(path) in ('.env', './.env'):  # pragma: no cover
                 settings_path = path
-            elif str(path).endswith('/.env'):
+            elif str(path).endswith('/.env'):  # pragma: no cover
                 settings_path = path
             elif str(path).endswith('.env'):  # pragma: no cover
                 settings_path = path.parent / '.env'
@@ -401,10 +436,12 @@ def validate(path):  # pragma: no cover
     # for each section register the validator for specific env
     # call validate
 
+    path = Path(path)
+
     if not str(path).endswith('.toml'):
         path = path / "dynaconf_validators.toml"
 
-    if not Path(path).exists():  # pragma: no cover  # noqa
+    if not path.exists():  # pragma: no cover  # noqa
         click.echo(click.style(
             "{} not found".format(path), fg="white", bg="red"
         ))
@@ -412,6 +449,7 @@ def validate(path):  # pragma: no cover
 
     validation_data = toml.load(open(str(path)))
 
+    success = True
     for env, name_data in validation_data.items():
         for name, data in name_data.items():
             if not isinstance(data, dict):  # pragma: no cover
@@ -419,15 +457,21 @@ def validate(path):  # pragma: no cover
                     "Invalid rule for parameter '{}'".format(name),
                     fg="white", bg="yellow"
                 ))
-            else:  # pragma: no cover
+            else:
                 data.setdefault('env', env)
                 click.echo(click.style(
                     "Validating '{}' with '{}'".format(name, data),
                     fg="white", bg="blue"
                 ))
-                Validator(name, **data).validate(settings)
-
-    # pragma: no cover
-    click.echo(click.style(
-        "Validation success!", fg="white", bg="green"
-    ))
+                try:
+                    Validator(name, **data).validate(settings)
+                except ValidationError as e:
+                    click.echo(click.style(
+                        "Error: {}".format(e), fg="white", bg="red"
+                    ))
+                    success = False
+
+    if success:
+        click.echo(click.style(
+            "Validation success!", fg="white", bg="green"
+        ))
diff --git a/dynaconf/default_settings.py b/dynaconf/default_settings.py
index bb0aa0a..0c2bc9c 100644
--- a/dynaconf/default_settings.py
+++ b/dynaconf/default_settings.py
@@ -85,6 +85,9 @@ GLOBAL_ENV_FOR_DYNACONF = get('GLOBAL_ENV_FOR_DYNACONF', 'DYNACONF')
 # The default encoding to open settings files
 ENCODING_FOR_DYNACONF = get('ENCODING_FOR_DYNACONF', 'utf-8')
 
+# Merge objects on load
+MERGE_ENABLED_FOR_DYNACONF = get('MERGE_ENABLED_FOR_DYNACONF', False)
+
 # The env var specifying settings module
 ENVVAR_FOR_DYNACONF = get('ENVVAR_FOR_DYNACONF',
                           'SETTINGS_MODULE_FOR_DYNACONF')
@@ -145,3 +148,8 @@ INI = get('INI', None)
 DOTENV_PATH_FOR_DYNACONF = get('DOTENV_PATH_FOR_DYNACONF', None)
 DOTENV_VERBOSE_FOR_DYNACONF = get('DOTENV_VERBOSE_FOR_DYNACONF', False)
 DOTENV_OVERRIDE_FOR_DYNACONF = get('DOTENV_OVERRIDE_FOR_DYNACONF', False)
+
+# Currently this is only used by cli. INSTANCE_FOR_DYNACONF specifies python
+# dotted path to custom LazySettings instance. Last dotted path item should be
+# instance of LazySettings.
+INSTANCE_FOR_DYNACONF = get('INSTANCE_FOR_DYNACONF', None)
diff --git a/dynaconf/loaders/__init__.py b/dynaconf/loaders/__init__.py
index 7d466b1..6284b88 100644
--- a/dynaconf/loaders/__init__.py
+++ b/dynaconf/loaders/__init__.py
@@ -50,20 +50,20 @@ def settings_loader(obj, settings_module=None, env=None,
     :param key: Load a single key if provided
     :param filename: optional filename to override the settings_module
     """
-    settings_module = settings_module or obj.settings_module
-    if not settings_module:  # pragma: no cover
-        return
+    if filename is None:
+        settings_module = settings_module or obj.settings_module
+        if not settings_module:  # pragma: no cover
+            return
 
-    if not isinstance(settings_module, (list, tuple)):
-        files = settings_module.split(',')
+        if not isinstance(settings_module, (list, tuple)):
+            files = settings_module.split(',')
+        else:
+            files = [settings_module]
     else:
-        files = [settings_module]
+        files = [filename]
 
     obj.logger.debug("Looking for %s", files)
 
-    if filename is not None:
-        files.append(filename)
-
     found_files = []
     modules_names = []
     for item in files:
diff --git a/dynaconf/loaders/ini_loader.py b/dynaconf/loaders/ini_loader.py
index 2587719..a454498 100644
--- a/dynaconf/loaders/ini_loader.py
+++ b/dynaconf/loaders/ini_loader.py
@@ -3,7 +3,7 @@ import io
 from dynaconf import default_settings
 from dynaconf.loaders.base import BaseLoader
 from dynaconf.constants import INI_EXTENSIONS
-from dynaconf.utils import dictmerge
+from dynaconf.utils import object_merge
 try:
     from configobj import ConfigObj
 except ImportError as e:  # pragma: no cover
@@ -44,7 +44,7 @@ def write(settings_path, settings_data, merge=True):
     :param merge: boolean if existing file should be merged with new data
     """
     if settings_path.exists() and merge:  # pragma: no cover
-        settings_data = dictmerge(
+        object_merge(
             ConfigObj(
                 io.open(
                     str(settings_path),
diff --git a/dynaconf/loaders/json_loader.py b/dynaconf/loaders/json_loader.py
index 83cb197..5b23f01 100644
--- a/dynaconf/loaders/json_loader.py
+++ b/dynaconf/loaders/json_loader.py
@@ -3,7 +3,7 @@ import io
 from dynaconf import default_settings
 from dynaconf.loaders.base import BaseLoader
 from dynaconf.constants import JSON_EXTENSIONS
-from dynaconf.utils import dictmerge
+from dynaconf.utils import object_merge
 import json
 
 
@@ -38,7 +38,7 @@ def write(settings_path, settings_data, merge=True):
     """
 
     if settings_path.exists() and merge:  # pragma: no cover
-        settings_data = dictmerge(
+        object_merge(
             json.load(
                 io.open(
                     str(settings_path),
diff --git a/dynaconf/loaders/py_loader.py b/dynaconf/loaders/py_loader.py
index 71d81b2..02cff23 100644
--- a/dynaconf/loaders/py_loader.py
+++ b/dynaconf/loaders/py_loader.py
@@ -5,7 +5,7 @@ import types
 import importlib
 from dynaconf import default_settings
 from dynaconf.utils.files import find_file
-from dynaconf.utils import dictmerge, DynaconfDict, raw_logger
+from dynaconf.utils import DynaconfDict, object_merge, raw_logger
 
 
 def load(obj, settings_module, identifier='py', silent=False, key=None):
@@ -89,7 +89,7 @@ def write(settings_path, settings_data, merge=True):
     if settings_path.exists() and merge:  # pragma: no cover
         existing = DynaconfDict()
         load(existing, str(settings_path))
-        settings_data = dictmerge(
+        object_merge(
             existing,
             settings_data
         )
diff --git a/dynaconf/loaders/toml_loader.py b/dynaconf/loaders/toml_loader.py
index cc8ce78..57aa4b8 100644
--- a/dynaconf/loaders/toml_loader.py
+++ b/dynaconf/loaders/toml_loader.py
@@ -3,7 +3,7 @@ import io
 from dynaconf import default_settings
 from dynaconf.loaders.base import BaseLoader
 from dynaconf.constants import TOML_EXTENSIONS
-from dynaconf.utils import dictmerge
+from dynaconf.utils import object_merge
 try:
     import toml
 except ImportError as e:  # pragma: no cover
@@ -44,7 +44,7 @@ def write(settings_path, settings_data, merge=True):
     :param merge: boolean if existing file should be merged with new data
     """
     if settings_path.exists() and merge:  # pragma: no cover
-        settings_data = dictmerge(
+        object_merge(
             toml.load(
                 io.open(
                     str(settings_path),
diff --git a/dynaconf/loaders/yaml_loader.py b/dynaconf/loaders/yaml_loader.py
index 1de9998..191603b 100644
--- a/dynaconf/loaders/yaml_loader.py
+++ b/dynaconf/loaders/yaml_loader.py
@@ -3,7 +3,7 @@ import io
 from dynaconf import default_settings
 from dynaconf.loaders.base import BaseLoader
 from dynaconf.constants import YAML_EXTENSIONS
-from dynaconf.utils import dictmerge
+from dynaconf.utils import object_merge
 try:
     import yaml
 except ImportError as e:  # pragma: no cover
@@ -44,7 +44,7 @@ def write(settings_path, settings_data, merge=True):
     :param merge: boolean if existing file should be merged with new data
     """
     if settings_path.exists() and merge:  # pragma: no cover
-        settings_data = dictmerge(
+        object_merge(
             yaml.load(
                 io.open(
                     str(settings_path),
diff --git a/dynaconf/utils/__init__.py b/dynaconf/utils/__init__.py
index 126e92e..2104d4f 100644
--- a/dynaconf/utils/__init__.py
+++ b/dynaconf/utils/__init__.py
@@ -12,17 +12,19 @@ BANNER = """
 """
 
 
-def dictmerge(old, new):
+def object_merge(old, new):
     """
-    Recursively update a dict.
-    Subdict's won't be overwritten but also updated.
+    Recursively merge two data structures
     """
-    for key, value in old.items():
-        if key not in new:
-            new[key] = value
-        elif isinstance(value, dict):
-            dictmerge(value, new[key])
-    return new
+    if isinstance(old, list) and isinstance(new, list):
+        for item in old[::-1]:
+            new.insert(0, item)
+    if isinstance(old, dict) and isinstance(new, dict):
+        for key, value in old.items():
+            if key not in new:
+                new[key] = value
+            else:
+                object_merge(value, new[key])
 
 
 class DynaconfDict(dict):
@@ -80,3 +82,32 @@ def compat_kwargs(kwargs):
                 old, new
             )
             kwargs[new] = kwargs[old]
+
+
+class Missing(object):
+    """
+    Sentinel value object/singleton used to differentiate between ambiguous
+    situations where `None` is a valid value.
+    """
+
+    def __bool__(self):
+        """Respond to boolean duck-typing."""
+        return False
+
+    def __eq__(self, other):
+        """Equality check for a singleton."""
+
+        return isinstance(other, self.__class__)
+
+    # Ensure compatibility with Python 2.x
+    __nonzero__ = __bool__
+
+    def __repr__(self):
+        """
+        Unambiguously identify this string-based representation of Missing,
+        used as a singleton.
+        """
+        return '<dynaconf.missing>'
+
+
+missing = Missing()
diff --git a/example/full_example.py b/example/full_example.py
index 99cf438..4578f22 100644
--- a/example/full_example.py
+++ b/example/full_example.py
@@ -54,4 +54,10 @@ print(settings.get('FOO', default='bar'))
 print("\nThe host for default :")
 print(settings.HOSTNAME)
 
+print("\nDotted path lookup value :")
+print(settings('ADICT.KEY'))
+
 print(settings.WORKS)
+
+print("\nDotted path set value :")
+print(settings.set('ONE.TWO', 'value'))
diff --git a/example/merge_configs/.env b/example/merge_configs/.env
new file mode 100644
index 0000000..304a338
--- /dev/null
+++ b/example/merge_configs/.env
@@ -0,0 +1,4 @@
+DYNACONF_ENV_VAR=1
+DYNACONF_WORKS=merge_configs
+MERGE_ENABLED_FOR_DYNACONF=true
+DYNACONF_A_BIG_DICT='@json {"file": {".env": true}}'
diff --git a/example/merge_configs/app.py b/example/merge_configs/app.py
new file mode 100644
index 0000000..bb43668
--- /dev/null
+++ b/example/merge_configs/app.py
@@ -0,0 +1,61 @@
+from dynaconf import settings
+
+print("Read from settings.py:", settings.PYTHON_VAR)  # noqa
+
+# BY DEFAULT 'development' is the current env
+print("Read from development_settings.py:", settings.PYTHON_DEV_VAR)  # noqa
+
+# If ENV_FOR_DYNACONF=production is in envvars so
+# print("Read from production_settings.py:", settings.PYTHON_PROD_VAR)  # noqa
+
+# global_ overrides previous configs
+print("Read from global_settings.py:", settings.PYTHON_GLOBAL_VAR)  # noqa
+
+
+print("Read from settings.yaml:", settings.YAML_VAR)  # noqa
+print("Read from settings.yml:", settings.YML_VAR)  # noqa
+print("Read from settings.toml:", settings.TOML_VAR)  # noqa
+print("Read from settings.tml:", settings.TML_VAR)  # noqa
+print("Read from settings.ini:", settings.INI_VAR)  # noqa
+print("Read from settings.conf:", settings.CONF_VAR)  # noqa
+print("Read from settings.properties:", settings.PROPERTIES_VAR)  # noqa
+print("Read from settings.json:", settings.JSON_VAR)  # noqa
+print("Read from .env:", settings.ENV_VAR)  # noqa
+print("Read from .env:", settings.WORKS)  # noqa
+
+# MERGE TESTS
+
+print("The dict should include all files", settings.A_BIG_DICT)  # noqa
+
+keys = (
+    'settings.py',
+    'development_settings.py',
+    'global_settings.py',
+    'settings.yaml',
+    'settings.yml',
+    'settings.toml.default',
+    'settings.toml.global',
+    'settings.tml.default',
+    'settings.tml.global',
+    'settings.ini',
+    'settings.conf',
+    'settings.properties',
+    'settings.json.default',
+    'settings.json.global',
+    '.env',
+)
+
+for key in keys:
+    assert key in settings.A_BIG_DICT.file
+    assert key in settings('A_BIG_DICT').file
+    assert key in settings('A_BIG_DICT')['file']
+    assert key in settings('A_BIG_DICT').get('file')
+    assert key in settings.A_BIG_DICT['file']
+    assert key in settings.A_BIG_DICT.get('file')
+
+
+print("Big nest", settings.A_BIG_DICT.nested_1.nested_2.nested_3.nested_4)  # noqa
+
+assert settings.A_BIG_DICT.nested_1.nested_2.nested_3.nested_4 == {
+    'json': True, 'yaml': True, 'toml': True, 'py': True
+}
diff --git a/example/merge_configs/development_settings.py b/example/merge_configs/development_settings.py
new file mode 100644
index 0000000..165319d
--- /dev/null
+++ b/example/merge_configs/development_settings.py
@@ -0,0 +1,2 @@
+PYTHON_DEV_VAR = 1
+A_BIG_DICT = {"file": {"development_settings.py": True}}
diff --git a/example/merge_configs/global_settings.py b/example/merge_configs/global_settings.py
new file mode 100644
index 0000000..7ae2826
--- /dev/null
+++ b/example/merge_configs/global_settings.py
@@ -0,0 +1,2 @@
+PYTHON_GLOBAL_VAR = 1
+A_BIG_DICT = {"file": {"global_settings.py": True}}
diff --git a/example/merge_configs/production_settings.py b/example/merge_configs/production_settings.py
new file mode 100644
index 0000000..6793302
--- /dev/null
+++ b/example/merge_configs/production_settings.py
@@ -0,0 +1,1 @@
+PYTHON_PROD_VAR = 1
diff --git a/example/merge_configs/settings.conf b/example/merge_configs/settings.conf
new file mode 100644
index 0000000..82b11a6
--- /dev/null
+++ b/example/merge_configs/settings.conf
@@ -0,0 +1,7 @@
+[default]
+conf_var=1
+
+
+  [[a_big_dict]]
+      [[[file]]]
+        settings.conf=True
diff --git a/example/merge_configs/settings.ini b/example/merge_configs/settings.ini
new file mode 100644
index 0000000..f783354
--- /dev/null
+++ b/example/merge_configs/settings.ini
@@ -0,0 +1,7 @@
+[default]
+ini_var=1
+
+  
+  [[a_big_dict]]
+      [[[file]]]
+        settings.ini=True
diff --git a/example/merge_configs/settings.json b/example/merge_configs/settings.json
new file mode 100644
index 0000000..29455d2
--- /dev/null
+++ b/example/merge_configs/settings.json
@@ -0,0 +1,22 @@
+{
+    "default": {
+        "json_var": 1, 
+        "a_big_dict": {
+            "file": {"settings.json.default": true},
+            "nested_1": {
+                "nested_2": {
+                    "nested_3": {
+                        "nested_4": {
+                            "json": true
+                        }
+                    }
+                }
+            } 
+        }
+    },
+    "global": { 
+        "a_big_dict": {
+            "file": {"settings.json.global": true}
+        }
+    }
+}
diff --git a/example/merge_configs/settings.properties b/example/merge_configs/settings.properties
new file mode 100644
index 0000000..849fd72
--- /dev/null
+++ b/example/merge_configs/settings.properties
@@ -0,0 +1,7 @@
+[default]
+properties_var=1
+
+
+  [[a_big_dict]]
+      [[[file]]]
+        settings.properties=True
diff --git a/example/merge_configs/settings.py b/example/merge_configs/settings.py
new file mode 100644
index 0000000..b1289b2
--- /dev/null
+++ b/example/merge_configs/settings.py
@@ -0,0 +1,14 @@
+PYTHON_VAR = 1
+A_BIG_DICT = {
+    "file": {"settings.py": True},
+    "never_erased": True,
+    "nested_1": {
+        "nested_2": {
+            "nested_3": {
+                "nested_4": {
+                    "py": True
+                }
+            }
+        }
+    }
+}
diff --git a/example/merge_configs/settings.tml b/example/merge_configs/settings.tml
new file mode 100644
index 0000000..79836c2
--- /dev/null
+++ b/example/merge_configs/settings.tml
@@ -0,0 +1,18 @@
+[default]
+tml_var=1
+
+  [default.a_big_dict.file]
+  'settings.tml.default' = true
+
+[production]
+tml_prod_var=2
+
+  [production.a_big_dict.file]
+  comment = 'This should not be merged unless ENV=production'
+  'settings.tml.production' = true
+
+[global]
+tml_global_var=3
+
+  [global.a_big_dict.file]
+  'settings.tml.global' = true
diff --git a/example/merge_configs/settings.toml b/example/merge_configs/settings.toml
new file mode 100644
index 0000000..12569ec
--- /dev/null
+++ b/example/merge_configs/settings.toml
@@ -0,0 +1,21 @@
+[default]
+toml_var=1
+
+  [default.a_big_dict.file]
+  'settings.toml.default' = true
+
+  [default.a_big_dict.nested_1.nested_2.nested_3.nested_4]
+  toml = true
+
+[production]
+toml_prod_var=2
+
+  [production.a_big_dict.file]
+  comment = 'This should not be merged unless ENV=production'
+  'settings.toml.production' = true
+
+[global]
+toml_global_var=3
+
+  [global.a_big_dict.file]
+  'settings.toml.global' = true
diff --git a/example/merge_configs/settings.yaml b/example/merge_configs/settings.yaml
new file mode 100644
index 0000000..1cb9749
--- /dev/null
+++ b/example/merge_configs/settings.yaml
@@ -0,0 +1,10 @@
+default:
+  yaml_var: 1
+  a_big_dict:
+    file:
+      settings.yaml: true
+    nested_1:
+      nested_2:
+        nested_3:
+          nested_4:
+            yaml: true
diff --git a/example/merge_configs/settings.yml b/example/merge_configs/settings.yml
new file mode 100644
index 0000000..e2e9a77
--- /dev/null
+++ b/example/merge_configs/settings.yml
@@ -0,0 +1,5 @@
+default:
+  yml_var: 1
+  a_big_dict:
+    file:
+      settings.yml: true
diff --git a/example/multiple_folders/.env b/example/multiple_folders/.env
new file mode 100644
index 0000000..16528c4
--- /dev/null
+++ b/example/multiple_folders/.env
@@ -0,0 +1,1 @@
+SETTINGS_MODULE_FOR_DYNACONF="config_1/settings.toml,config_2/settings.toml"
diff --git a/example/multiple_folders/app.py b/example/multiple_folders/app.py
new file mode 100644
index 0000000..7875766
--- /dev/null
+++ b/example/multiple_folders/app.py
@@ -0,0 +1,10 @@
+from dynaconf import settings
+
+print('development')
+print(settings.VAR1)
+print(settings.VAR2)
+
+print('production')
+settings.setenv('production')
+print(settings.VAR1)
+print(settings.VAR2)
diff --git a/example/multiple_folders/config_1/__init__.py b/example/multiple_folders/config_1/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/example/multiple_folders/config_1/settings.toml b/example/multiple_folders/config_1/settings.toml
new file mode 100644
index 0000000..0ed31d7
--- /dev/null
+++ b/example/multiple_folders/config_1/settings.toml
@@ -0,0 +1,4 @@
+[development]
+VAR1 = 'var1_dev'
+[production]
+VAR1 = 'var1_prod'
diff --git a/example/multiple_folders/config_2/__init__.py b/example/multiple_folders/config_2/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/example/multiple_folders/config_2/settings.toml b/example/multiple_folders/config_2/settings.toml
new file mode 100644
index 0000000..4825cc6
--- /dev/null
+++ b/example/multiple_folders/config_2/settings.toml
@@ -0,0 +1,4 @@
+[development]
+VAR2 = 'var2_dev'
+[production]
+VAR2 = 'var2_prod'
diff --git a/example/validators/with_python/conf.py b/example/validators/with_python/conf.py
index 1a914d8..177d465 100644
--- a/example/validators/with_python/conf.py
+++ b/example/validators/with_python/conf.py
@@ -55,7 +55,12 @@ settings.validators.register(
         )
     ),
     Validator('PORT', must_exist=True, ne=8000, when=Validator('MYSQL_HOST',
-                                                               eq='localhost'))
+                                                               eq='localhost')),
+    Validator(
+        'A_DICT.NESTED_1.NESTED_2.NESTED_3.NESTED_4',
+        must_exist=True, eq=1
+    )
+
 )
 
 settings.validators.validate()
diff --git a/example/validators/with_python/settings.py b/example/validators/with_python/settings.py
index 20603d8..8b7e3e6 100644
--- a/example/validators/with_python/settings.py
+++ b/example/validators/with_python/settings.py
@@ -14,3 +14,13 @@ PROJECT = 'hello_world'
 SALARY = 2000
 
 WORKS = 'validator'
+
+A_DICT = {
+    'NESTED_1': {
+        'NESTED_2': {
+            'NESTED_3': {
+                'NESTED_4': 1
+            }
+        }
+    }
+}
diff --git a/example/validators/with_toml/dynaconf_validators.toml b/example/validators/with_toml/dynaconf_validators.toml
index 65454ec..5c47408 100644
--- a/example/validators/with_toml/dynaconf_validators.toml
+++ b/example/validators/with_toml/dynaconf_validators.toml
@@ -3,6 +3,9 @@ version = {must_exist=true}
 name = {must_exist=true}
 password = {must_exist=false}
 
+# using dot notation
+'a_big_dict.nested_1.nested_2.nested_3.nested_4' = {must_exist=true, eq=1}
+
 # invalid rule, must always be a dict
 a = 1
 
diff --git a/example/validators/with_toml/settings.toml b/example/validators/with_toml/settings.toml
index 784f030..e5145ba 100644
--- a/example/validators/with_toml/settings.toml
+++ b/example/validators/with_toml/settings.toml
@@ -3,6 +3,9 @@ version = "1.0.0"
 name = "Dynaconf"
 age = 15
 
+  [default.a_big_dict.nested_1.nested_2.nested_3]
+  nested_4 = 1
+
 [production]
 project = "hello_world"
 password = 'exists only in prod'
diff --git a/tox.ini b/tox.ini
index fa55ec8..92f2610 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py34,py35,py36
+envlist = py34,py35,py36,py37
 whitelist_externals=make
 
 [testenv]
@@ -9,6 +9,6 @@ whitelist_externals=
     cd
     python
 commands =
-    py.test --boxed -v --cov=dynaconf -l --tb=short --maxfail=1 tests/
+    py.test --boxed -v --cov=dynaconf -l --tb=short --maxfail=1 {posargs}
     ; make test_examples
 


Test patch:
diff --git a/tests/test_base.py b/tests/test_base.py
index 1e2e24d..2453394 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -10,17 +10,17 @@ def test_deleted_raise(settings):
     del settings.TODELETE
     with pytest.raises(AttributeError):
         assert settings.TODELETE is True
-    assert settings.exists('TODELETE') is False
-    assert settings.get('TODELETE') is None
+    assert settings.exists("TODELETE") is False
+    assert settings.get("TODELETE") is None
 
 
 def test_accepts_only_upper(settings):
     """Only upper case names are allowed
     lower case are converted"""
     assert settings.DEBUG is True
-    assert settings.get('debug') is True
-    assert settings.get('DEBUG') is True
-    assert settings.exists('debug')
+    assert settings.get("debug") is True
+    assert settings.get("DEBUG") is True
+    assert settings.exists("debug")
     with pytest.raises(AttributeError):
         # access in lower case is not allowed
         assert settings.debug is True
@@ -29,11 +29,11 @@ def test_accepts_only_upper(settings):
 def test_call_works_as_get(settings):
     """settings.get('name') is the same as settings('name')"""
 
-    assert settings('debug') == settings.get('DEBUG')
-    assert settings('non_exist', default='hello') == 'hello'
-    assert settings.get('non_exist', default='hello') == 'hello'
-    assert settings.__call__('debug') == settings.DEBUG
-    assert settings._wrapped.__call__('debug') == settings.DEBUG
+    assert settings("debug") == settings.get("DEBUG")
+    assert settings("non_exist", default="hello") == "hello"
+    assert settings.get("non_exist", default="hello") == "hello"
+    assert settings.__call__("debug") == settings.DEBUG
+    assert settings._wrapped.__call__("debug") == settings.DEBUG
 
 
 def test_keys_are_equal(settings):
@@ -46,89 +46,89 @@ def test_values_are_equal(settings):
 
 
 def test_get_env(settings):
-    settings.environ['FRUIT'] = 'BANANA'
-    assert settings.exists_in_environ('fruit') is True
-    assert settings.environ['FRUIT'] == settings.get_environ('fruit')
-    assert os.environ['FRUIT'] == settings.environ.get('FRUIT')
+    settings.environ["FRUIT"] = "BANANA"
+    assert settings.exists_in_environ("fruit") is True
+    assert settings.environ["FRUIT"] == settings.get_environ("fruit")
+    assert os.environ["FRUIT"] == settings.environ.get("FRUIT")
 
-    settings.environ['SALARY'] = '180.235'
-    assert settings.get_environ('salary', cast='@float') == 180.235
+    settings.environ["SALARY"] = "180.235"
+    assert settings.get_environ("salary", cast="@float") == 180.235
 
-    settings.environ['ENVINT'] = '@int 24'
-    assert settings.get_environ('envint', cast=True) == 24
+    settings.environ["ENVINT"] = "@int 24"
+    assert settings.get_environ("envint", cast=True) == 24
 
 
 def test_float(settings):
-    settings.set('money', '500.42')
-    assert settings.exists('MONEY')
-    assert settings.MONEY == '500.42'
+    settings.set("money", "500.42")
+    assert settings.exists("MONEY")
+    assert settings.MONEY == "500.42"
     assert settings.MONEY != 500.42
-    assert settings.store['MONEY'] == '500.42'
-    assert 'MONEY' not in settings._deleted
-    assert 'money' not in settings._deleted
-    assert isinstance(settings.as_float('money'), float)
-    assert settings.as_float('MONEY') == 500.42
+    assert settings.store["MONEY"] == "500.42"
+    assert "MONEY" not in settings._deleted
+    assert "money" not in settings._deleted
+    assert isinstance(settings.as_float("money"), float)
+    assert settings.as_float("MONEY") == 500.42
 
 
 def test_int(settings):
-    settings.set('age', '500')
-    assert settings.exists('AGE')
-    assert settings.AGE == '500'
+    settings.set("age", "500")
+    assert settings.exists("AGE")
+    assert settings.AGE == "500"
     assert settings.AGE != 500
-    assert settings.store['AGE'] == '500'
-    assert 'AGE' not in settings._deleted
-    assert 'age' not in settings._deleted
-    assert isinstance(settings.as_int('age'), int)
-    assert settings.as_int('age') == 500
+    assert settings.store["AGE"] == "500"
+    assert "AGE" not in settings._deleted
+    assert "age" not in settings._deleted
+    assert isinstance(settings.as_int("age"), int)
+    assert settings.as_int("age") == 500
 
 
 def test_bool(settings):
     for true_value in true_values:
         # ('t', 'true', 'enabled', '1', 'on', 'yes')
-        settings.set('feature', true_value)
-        assert settings.exists('FEATURE')
+        settings.set("feature", true_value)
+        assert settings.exists("FEATURE")
         assert settings.FEATURE == true_value
         assert settings.FEATURE is not True
-        assert settings.store['FEATURE'] == true_value
-        assert 'FEATURE' not in settings._deleted
-        assert 'feature' not in settings._deleted
-        assert isinstance(settings.as_bool('feature'), bool)
-        assert settings.as_bool('FEATURE') is True
+        assert settings.store["FEATURE"] == true_value
+        assert "FEATURE" not in settings._deleted
+        assert "feature" not in settings._deleted
+        assert isinstance(settings.as_bool("feature"), bool)
+        assert settings.as_bool("FEATURE") is True
 
     # anything else is a false value
-    false_values = ['f', 'false', 'False', 'disabled', '0', 'off', 'no']
+    false_values = ["f", "false", "False", "disabled", "0", "off", "no"]
     for false_value in false_values:
-        settings.set('feature', false_value)
-        assert settings.exists('FEATURE')
+        settings.set("feature", false_value)
+        assert settings.exists("FEATURE")
         assert settings.FEATURE == false_value
         assert settings.FEATURE is not False
-        assert settings.store['FEATURE'] == false_value
-        assert 'FEATURE' not in settings._deleted
-        assert 'feature' not in settings._deleted
-        assert isinstance(settings.as_bool('feature'), bool)
-        assert settings.as_bool('FEATURE') is False
+        assert settings.store["FEATURE"] == false_value
+        assert "FEATURE" not in settings._deleted
+        assert "feature" not in settings._deleted
+        assert isinstance(settings.as_bool("feature"), bool)
+        assert settings.as_bool("FEATURE") is False
 
 
 def test_as_json(settings):
-    settings.set('fruits', '["banana", "apple", "kiwi"]')
-    assert settings.exists('FRUITS')
+    settings.set("fruits", '["banana", "apple", "kiwi"]')
+    assert settings.exists("FRUITS")
     assert settings.FRUITS == '["banana", "apple", "kiwi"]'
     assert settings.FRUITS != ["banana", "apple", "kiwi"]
-    assert settings.store['FRUITS'] == '["banana", "apple", "kiwi"]'
-    assert 'FRUITS' not in settings._deleted
-    assert 'fruits' not in settings._deleted
-    assert isinstance(settings.as_json('fruits'), list)
-    assert settings.as_json('fruits') == ["banana", "apple", "kiwi"]
-
-    settings.set('person', '{"name": "Bruno"}')
-    assert settings.exists('PERSON')
+    assert settings.store["FRUITS"] == '["banana", "apple", "kiwi"]'
+    assert "FRUITS" not in settings._deleted
+    assert "fruits" not in settings._deleted
+    assert isinstance(settings.as_json("fruits"), list)
+    assert settings.as_json("fruits") == ["banana", "apple", "kiwi"]
+
+    settings.set("person", '{"name": "Bruno"}')
+    assert settings.exists("PERSON")
     assert settings.PERSON == '{"name": "Bruno"}'
     assert settings.PERSON != {"name": "Bruno"}
-    assert settings.store['PERSON'] == '{"name": "Bruno"}'
-    assert 'PERSON' not in settings._deleted
-    assert 'person' not in settings._deleted
-    assert isinstance(settings.as_json('person'), dict)
-    assert settings.as_json('person') == {"name": "Bruno"}
+    assert settings.store["PERSON"] == '{"name": "Bruno"}'
+    assert "PERSON" not in settings._deleted
+    assert "person" not in settings._deleted
+    assert isinstance(settings.as_json("person"), dict)
+    assert settings.as_json("person") == {"name": "Bruno"}
 
 
 def test_env_should_be_string(settings):
@@ -139,40 +139,165 @@ def test_env_should_be_string(settings):
 
 def test_env_should_not_have_underline(settings):
     with pytest.raises(AttributeError):
-        with settings.setenv('COOL_env'):
+        with settings.setenv("COOL_env"):
             pass
 
 
 def test_path_for(settings):
-    assert settings.path_for('/tmp', 'bla') == '/tmp/bla'
-    assert settings.path_for('foo', 'bar', 'blaz') == './foo/bar/blaz'
+    assert settings.path_for("/tmp", "bla") == "/tmp/bla"
+    assert settings.path_for("foo", "bar", "blaz") == "./foo/bar/blaz"
 
 
 def test_get_item(settings):
-    assert settings['DOTENV_INT'] == 1
-    assert settings['PORT'] == 5000
+    assert settings["DOTENV_INT"] == 1
+    assert settings["PORT"] == 5000
     with pytest.raises(KeyError):
-        settings['DONOTEXISTTHISKEY']
+        settings["DONOTEXISTTHISKEY"]
 
 
 def test_set_item(settings):
-    settings['FOO'] = 'bar'
-    assert settings.FOO == 'bar'
-    assert 'FOO' in settings._defaults
-    assert settings('FOO') == 'bar'
-    assert settings.get('FOO') == 'bar'
+    settings["FOO"] = "bar"
+    assert settings.FOO == "bar"
+    assert "FOO" in settings._defaults
+    assert settings("FOO") == "bar"
+    assert settings.get("FOO") == "bar"
 
 
 def test_set(settings):
     # NOTE: it is recommended to call set(x, 1) or ['x'] = 1
     # instead of settings.BAZ = 'bar'
-    settings.set('BAZ', 'bar')
-    assert settings.BAZ == 'bar'
-    assert 'BAZ' in settings._defaults
-    assert settings('BAZ') == 'bar'
-    assert settings.get('BAZ') == 'bar'
+    settings.set("BAZ", "bar")
+    assert settings.BAZ == "bar"
+    assert "BAZ" in settings._defaults
+    assert settings("BAZ") == "bar"
+    assert settings.get("BAZ") == "bar"
+
+
+def test_set_merge(settings):
+    settings.set("MERGE_ENABLED_FOR_DYNACONF", True)
+    settings.set("MERGE_KEY", {
+        "items": [{"name": "item 1"}, {"name": "item 2"}]
+    })
+    settings.set("MERGE_KEY", {
+        "items": [{"name": "item 3"}, {"name": "item 4"}]
+    })
+    assert settings.MERGE_KEY == {
+        "items": [
+            {"name": "item 1"}, {"name": "item 2"},
+            {"name": "item 3"}, {"name": "item 4"}
+        ]
+    }
 
 
 def test_exists(settings):
     settings.set('BOOK', 'TAOCP')
-    assert settings.exists('BOOK') == True
+    assert settings.exists('BOOK') is True
+
+
+def test_dotted_traversal_access(settings):
+    settings.set(
+        'PARAMS',
+        {
+            'PASSWORD': 'secret',
+            'SSL': {'CONTEXT': 'SECURE'},
+            'DOTTED.KEY': True
+        },
+        dotted_lookup=False
+    )
+    assert settings.get('PARAMS') == {
+        'PASSWORD': 'secret',
+        'SSL': {
+            'CONTEXT': 'SECURE'
+        },
+        'DOTTED.KEY': True
+    }
+
+    assert settings('PARAMS.PASSWORD') == 'secret'
+    assert settings('PARAMS.SSL.CONTEXT') == 'SECURE'
+    assert settings('PARAMS.TOKEN.IMAGINARY', 1234) == 1234
+    assert settings('IMAGINARY_KEY.FOO') is None
+    assert settings('IMAGINARY_KEY') is None
+
+    assert settings['PARAMS.PASSWORD'] == 'secret'
+    assert settings['PARAMS.SSL.CONTEXT'] == 'SECURE'
+    assert settings.PARAMS.SSL.CONTEXT == 'SECURE'
+
+    assert settings.exists("PARAMS") is True
+    assert settings.exists("PARAMS.PASSWORD") is True
+    assert settings.exists("PARAMS.SSL") is True
+    assert settings.exists("PARAMS.SSL.FAKE") is False
+    assert settings.exists("PARAMS.SSL.CONTEXT") is True
+
+    # Dotted traversal should not work for dictionary-like key access.
+    with pytest.raises(KeyError):
+        settings['PARAMS.DOESNOTEXIST']
+
+    # Disable dot-traversal on a per-call basis.
+    assert settings('PARAMS.PASSWORD', dotted_lookup=False) is None
+
+    assert settings('PARAMS.DOTTED.KEY') is None
+    assert settings('PARAMS').get('DOTTED.KEY') is True
+
+    settings.set('DOTTED.KEY', True, dotted_lookup=False)
+    assert settings('DOTTED.KEY', dotted_lookup=False) is True
+
+    settings.set(
+        'NESTED_1',
+        {
+            "nested_2": {
+                "nested_3": {
+                    "nested_4": True
+                }
+            }
+        }
+    )
+
+    assert settings.NESTED_1.nested_2.nested_3.nested_4 is True
+    assert settings['NESTED_1.nested_2.nested_3.nested_4'] is True
+    assert settings('NESTED_1.nested_2.nested_3.nested_4') is True
+    # First key is always transformed to upper()
+    assert settings('nested_1.nested_2.nested_3.nested_4') is True
+
+
+def test_dotted_set(settings):
+
+    settings.set('nested_1.nested_2.nested_3.nested_4', 'secret')
+
+    assert settings.NESTED_1.NESTED_2.NESTED_3.NESTED_4 == 'secret'
+    assert settings.NESTED_1.NESTED_2.NESTED_3.to_dict() == {'nested_4': 'secret'}
+    assert settings.NESTED_1.NESTED_2.to_dict() == {
+        'nested_3': {
+            'nested_4': 'secret'
+        }
+    }
+
+    assert settings.get('nested_1').to_dict() == {
+        'nested_2': {
+            'nested_3': {
+                'nested_4': 'secret'
+            }
+        }
+    }
+
+    with pytest.raises(KeyError):
+        settings.NESTED_1.NESTED_5
+
+
+def test_dotted_set_with_merge(settings):
+    settings.set("MERGE_ENABLED_FOR_DYNACONF", True)
+
+    settings.set("MERGE.KEY", {
+        "items": [{"name": "item 1"}, {"name": "item 2"}]
+    })
+    settings.set("MERGE.KEY", {
+        "items": [{"name": "item 3"}, {"name": "item 4"}]
+    })
+
+    assert settings.MERGE.KEY == {
+        'items': [
+            {'name': 'item 1'},
+            {'name': 'item 2'},
+            {'name': 'item 3'},
+            {'name': 'item 4'},
+        ]
+    }
diff --git a/tests/test_cli.py b/tests/test_cli.py
index ff64286..d1ec6f1 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -3,7 +3,7 @@ import os
 import pytest
 from pathlib import Path
 from click.testing import CliRunner
-from dynaconf import default_settings
+from dynaconf import LazySettings, default_settings
 from dynaconf.cli import main, EXTS, WRITERS, ENVS, read_file_in_root_directory
 from dotenv import cli as dotenv_cli
 
@@ -11,8 +11,8 @@ from dotenv import cli as dotenv_cli
 runner = CliRunner()
 
 
-def run(cmd):
-    result = runner.invoke(main, cmd)
+def run(cmd, env=None):
+    result = runner.invoke(main, cmd, env=env, catch_exceptions=False)
     # assert result.exit_code == 0
     return result.output
 
@@ -87,9 +87,9 @@ def test_init_with_path(fileformat, tmpdir):
             [
                 'init',
                 '--format={}'.format(fileformat),
-                '--path={}'.format(str(path)),
+                '--path={}'.format(str(tmpdir)),
                 '-y'
-            ]
+            ],
         )
 
     sets = Path(str(path))
@@ -147,6 +147,61 @@ def test_list_with_env(env):
     assert "Working in {} environment".format(env) in result
 
 
+settings = LazySettings(OPTION_FOR_TESTS=True)
+
+
+def test_list_with_instance():
+    result = run(
+        [
+            '-i', 'tests.test_cli.settings',
+            'list',
+        ]
+    )
+    assert "OPTION_FOR_TESTS: True" in result
+
+
+def test_list_with_instance_from_env():
+    result = run(
+        [
+            'list',
+        ],
+        {
+            'INSTANCE_FOR_DYNACONF': 'tests.test_cli.settings',
+        }
+    )
+    assert "OPTION_FOR_TESTS: True" in result
+
+
+def test_instance_attribute_error():
+    result = run(
+        [
+            '-i', 'tests.test_cli.idontexist',
+            'list',
+        ]
+    )
+    assert "has no attribute 'idontexist'" in result
+
+
+def test_instance_import_error():
+    result = run(
+        [
+            '-i', 'idontexist.settings',
+            'list',
+        ]
+    )
+    assert "Error: No module named 'idontexist'" in result
+
+
+def test_instance_pypath_error():
+    result = run(
+        [
+            '-i', 'idontexist',
+            'list',
+        ]
+    )
+    assert "Error: invalid path to settings instance: idontexist" in result
+
+
 def test_list_with_key():
     result = run(
         [
@@ -292,21 +347,31 @@ def test_validate(tmpdir):
     toml_invalid = tmpdir.mkdir('invalid').join('settings.toml')
     toml_invalid.write(TOML_INVALID)
 
-    os.chdir(str(Path(str(toml_valid)).parent))
     result = run(
         [
             'validate',
             '-p',
             str(validation_file)
-        ]
+        ],
+        {
+            'SETTINGS_MODULE_FOR_DYNACONF': str(toml_valid),
+        }
     )
+    assert "Validation success!" in result
 
-    os.chdir(str(Path(str(toml_invalid)).parent))
     result = run(
         [
             'validate',
             '-p',
             str(Path(str(validation_file)).parent)
-        ]
+        ],
+        {
+            'SETTINGS_MODULE_FOR_DYNACONF': str(toml_invalid),
+        }
     )
-    print(result)
+    assert "age must lte 30 but it is 35 in env default" in result
+    assert "project must eq hello_world but it is This is not hello_world " \
+           "in env production" in result
+    assert "host must is_not_in ['test.com'] but it is test.com in env " \
+           "production" in result
+    assert "Validation success!" not in result
diff --git a/tests/test_nested_loading.py b/tests/test_nested_loading.py
new file mode 100644
index 0000000..a65e35d
--- /dev/null
+++ b/tests/test_nested_loading.py
@@ -0,0 +1,257 @@
+import pytest
+from dynaconf.base import LazySettings
+
+
+TOML = """
+[default]
+dynaconf_include = ["plugin1.toml", "plugin2.toml", "plugin2.toml"]
+DEBUG = false
+SERVER = "base.example.com"
+PORT = 6666
+
+[development]
+DEBUG = false
+SERVER = "dev.example.com"
+
+[production]
+DEBUG = false
+SERVER = "prod.example.com"
+"""
+
+MIXED = """
+[default]
+dynaconf_include = ["plugin1.toml", "plugin2.{0}"]
+DEBUG = false
+SERVER = "base.example.com"
+PORT = 6666
+
+[development]
+DEBUG = false
+SERVER = "dev.example.com"
+
+[production]
+DEBUG = false
+SERVER = "prod.example.com"
+"""
+
+MIXED_MERGE = """
+[default]
+dynaconf_include = ["plugin1.toml", "plugin2.json", "plugin2.yaml", "plugin2.ini", "plugin2.py"]
+DEBUG = false
+SERVER = "base.example.com"
+PORT = 6666
+
+[development]
+DEBUG = false
+SERVER = "dev.example.com"
+
+[production]
+DEBUG = false
+SERVER = "prod.example.com"
+
+
+[custom.nested_1]
+base = 1
+
+[custom.nested_1.nested_2]
+base = 2
+
+[custom.nested_1.nested_2.nested_3]
+base = 3
+
+[custom.nested_1.nested_2.nested_3.nested_4]
+base = 4
+"""
+
+TOML_PLUGIN = """
+[default]
+SERVER = "toml.example.com"
+PLUGIN_NAME = "testing"
+
+[development]
+SERVER = "toml.example.com"
+PLUGIN = "extra development var"
+
+[production]
+SERVER = "toml.example.com"
+PLUGIN = "extra production var"
+
+[custom.nested_1.nested_2.nested_3.nested_4]
+toml = 5
+"""
+
+TOML_PLUGIN_2 = """
+[default]
+SERVER = "plugin2.example.com"
+PLUGIN_2_SPECIAL = true
+PORT = 4040
+
+[custom.nested_1.nested_2.nested_3.nested_4]
+toml = 5
+"""
+
+TOML_PLUGIN_TEXT = """
+[default]
+database_uri = "toml.example.com"
+port = 8080
+
+[custom.nested_1.nested_2.nested_3.nested_4]
+toml = 5
+"""
+
+JSON_PLUGIN_TEXT = """
+{
+    "default": {
+        "database_uri": "json.example.com",
+        "port": 8080
+    },
+    "custom": {
+        "nested_1": {
+            "nested_2": {
+                "nested_3": {
+                    "nested_4": {
+                        "json": 5
+                    }
+                }
+            }
+        }
+    }
+}
+"""
+
+YAML_PLUGIN_TEXT = """
+default:
+  database_uri: "yaml.example.com"
+  port: 8080
+
+custom:
+  nested_1:
+    nested_2:
+      nested_3:
+        nested_4:
+          yaml: 5
+"""
+
+INI_PLUGIN_TEXT = """
+[default]
+database_uri="ini.example.com"
+port="@int 8080"
+
+[custom]
+  [[nested_1]]
+    [[[nested_2]]]
+      [[[[nested_3]]]]
+          [[[[[nested_4]]]]]
+            ini="@int 5"
+"""
+
+PY_PLUGIN_TEXT = """
+DATABASE_URI = "py.example.com"
+PORT = 8080
+NESTED_1 = {
+    "nested_2": {
+        "nested_3": {
+            "nested_4": {
+                "py": 5
+            }
+        }
+    }
+}
+"""
+
+PLUGIN_TEXT = {
+    'toml': TOML_PLUGIN_TEXT,
+    'yaml': YAML_PLUGIN_TEXT,
+    'json': JSON_PLUGIN_TEXT,
+    'ini': INI_PLUGIN_TEXT,
+    'py':  PY_PLUGIN_TEXT
+}
+
+
+def test_load_nested_toml(tmpdir):
+    """Load a TOML file that includes other TOML files."""
+
+    settings_file = tmpdir.join("settings.toml")
+    settings_file.write(TOML)
+
+    toml_plugin_file = tmpdir.join("plugin1.toml")
+    toml_plugin_file.write(TOML_PLUGIN)
+
+    toml_plugin_file = tmpdir.join("plugin2.toml")
+    toml_plugin_file.write(TOML_PLUGIN_2)
+
+    settings = LazySettings(
+        ENV_FOR_DYNACONF="DEFAULT",
+        silent=False,
+        LOADERS_FOR_DYNACONF=False,
+        SETTINGS_MODULE_FOR_DYNACONF=str(settings_file))
+
+    # Ensure overrides that happen via TOML plugin config load.
+    assert settings.SERVER == "plugin2.example.com"
+    assert settings.DEBUG is False
+    assert settings.PLUGIN_NAME == "testing"
+
+    # From the second TOML plugin
+    assert settings.PORT == 4040
+    assert settings.PLUGIN_2_SPECIAL is True
+
+
+@pytest.mark.parametrize('ext', ['toml', 'json', 'yaml', 'ini', 'py'])
+def test_load_nested_different_types(ext, tmpdir):
+    """Load a TOML file that includes other various settings file types."""
+
+    settings_file = tmpdir.join("settings.toml")
+    settings_file.write(MIXED.format(ext))
+
+    toml_plugin_file = tmpdir.join("plugin1.toml")
+    toml_plugin_file.write(TOML_PLUGIN)
+
+    json_plugin_file = tmpdir.join("plugin2.{0}".format(ext))
+    json_plugin_file.write(PLUGIN_TEXT[ext])
+
+    settings = LazySettings(
+        ENV_FOR_DYNACONF="DEFAULT",
+        silent=False,
+        LOADERS_FOR_DYNACONF=False,
+        SETTINGS_MODULE_FOR_DYNACONF=str(settings_file))
+
+    assert settings.DEBUG is False
+    assert settings.DATABASE_URI == "{0}.example.com".format(ext)
+    assert settings.PORT == 8080
+    assert settings.SERVER == "toml.example.com"
+    assert settings.PLUGIN_NAME == "testing"
+
+
+def test_load_nested_different_types_with_merge(tmpdir):
+    """Check merge works for includes."""
+
+    settings_file = tmpdir.join("settings.toml")
+    settings_file.write(MIXED_MERGE)
+
+    toml_plugin_file = tmpdir.join("plugin1.toml")
+    toml_plugin_file.write(TOML_PLUGIN)
+
+    for ext in ['toml', 'json', 'yaml', 'ini', 'py']:
+        json_plugin_file = tmpdir.join("plugin2.{0}".format(ext))
+        json_plugin_file.write(PLUGIN_TEXT[ext])
+
+    settings = LazySettings(
+        ENV_FOR_DYNACONF="custom",
+        silent=False,
+        LOADERS_FOR_DYNACONF=False,
+        SETTINGS_MODULE_FOR_DYNACONF=str(settings_file),
+        MERGE_ENABLED_FOR_DYNACONF=True)
+
+    assert settings.DEBUG is False
+    assert settings.DATABASE_URI == "{0}.example.com".format(ext)
+    assert settings.PORT == 8080
+    assert settings.SERVER == "toml.example.com"
+    assert settings.PLUGIN_NAME == "testing"
+
+    # merge asserts
+    assert settings.NESTED_1.base == 1
+    assert settings.NESTED_1.nested_2.base == 2
+    assert settings.NESTED_1.nested_2.nested_3.base == 3
+    assert settings.NESTED_1.nested_2.nested_3.nested_4.base == 4
+    for ext in ['toml', 'json', 'yaml', 'ini', 'py']:
+        assert settings.NESTED_1.nested_2.nested_3.nested_4[ext] == 5
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 4536477..7c39abd 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -4,6 +4,7 @@ import os
 import pytest
 import tempfile
 from dynaconf import default_settings
+from dynaconf.utils import missing, Missing
 from dynaconf.utils.parse_conf import unparse_conf_data, parse_conf_data
 from dynaconf.utils.files import find_file
 
@@ -68,12 +69,13 @@ def test_find_file():
     assert find_file(usecwd=True) == filename
 
 
-def test_disable_cast():
+def test_disable_cast(monkeypatch):
     # this casts for int
     assert parse_conf_data('@int 42') == 42
     # now gives pure string
-    os.environ['AUTO_CAST_FOR_DYNACONF'] = 'off'
-    assert parse_conf_data('@int 42') == '@int 42'
+    with monkeypatch.context() as m:
+        m.setenv('AUTO_CAST_FOR_DYNACONF', 'off')
+        assert parse_conf_data('@int 42') == '@int 42'
 
 
 def test_tomlfy():
@@ -86,4 +88,24 @@ def test_tomlfy():
     assert parse_conf_data("['a', 'b', 'c']", tomlfy=True) == ['a', 'b', 'c']
     assert parse_conf_data("[true, false]", tomlfy=True) == [True, False]
     assert parse_conf_data("{key='value', v=1}", tomlfy=True
-        ) == {'key': 'value', 'v': 1}
+                           ) == {'key': 'value', 'v': 1}
+
+
+def test_missing_sentinel():
+
+    # The missing singleton should always compare truthfully to itself
+    assert missing == missing
+
+    # new instances of Missing should be equal to each other due to
+    # explicit __eq__ implmentation check for isinstance.
+    assert missing == Missing()
+
+    # The sentinel should not be equal to None, True, or False
+    assert missing is not None
+    assert missing is not True
+    assert missing is not False
+
+    # But the explict typecasting of missing to a bool should evaluate to False
+    assert bool(missing) is False
+
+    assert str(missing) == '<dynaconf.missing>'
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 03e6a8a..fcd164d 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -135,6 +135,30 @@ def test_validators(tmpdir):
         Validator('A', when=1)
 
 
+def test_dotted_validators(settings):
+
+    settings.set(
+        'PARAMS',
+        {
+            'PASSWORD': 'secret',
+            'SSL': {
+                'CONTEXT': 'SECURE',
+                'ENABLED': True
+            },
+        }
+    )
+
+    settings.validators.register(
+        Validator('PARAMS', must_exist=True,  is_type_of=dict),
+        Validator('PARAMS.PASSWORD', must_exist=True, is_type_of=str),
+        Validator('PARAMS.SSL', must_exist=True, is_type_of=dict),
+        Validator('PARAMS.SSL.ENABLED', must_exist=True, is_type_of=bool),
+        Validator('PARAMS.NONEXISTENT', must_exist=False, is_type_of=str)
+    )
+
+    assert settings.validators.validate() is None
+
+
 @pytest.mark.parametrize(
     'validator_instance',
     [