Repository: rochacbruno/dynaconf
Pull request #100 (2018-10-24 14:35:23)

Fix small typo in README.md


Patch:
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 3ea6bb4..438fc02 100644
--- a/Makefile
+++ b/Makefile
@@ -45,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/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 2b477fe..a1ed4d7 100644
--- a/docs/guides/configuration.md
+++ b/docs/guides/configuration.md
@@ -49,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/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 00e125c..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
 
@@ -83,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):
@@ -113,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
@@ -514,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
@@ -522,6 +538,14 @@ 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()
 
@@ -610,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 d62ced6..0c2bc9c 100644
--- a/dynaconf/default_settings.py
+++ b/dynaconf/default_settings.py
@@ -148,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/example/full_example.py b/example/full_example.py
index b601350..4578f22 100644
--- a/example/full_example.py
+++ b/example/full_example.py
@@ -58,3 +58,6 @@ print("\nDotted path lookup value :")
 print(settings('ADICT.KEY'))
 
 print(settings.WORKS)
+
+print("\nDotted path set value :")
+print(settings.set('ONE.TWO', 'value'))


Test patch:
diff --git a/tests/test_base.py b/tests/test_base.py
index a0ede2f..2453394 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -201,7 +201,8 @@ def test_dotted_traversal_access(settings):
             'PASSWORD': 'secret',
             'SSL': {'CONTEXT': 'SECURE'},
             'DOTTED.KEY': True
-        }
+        },
+        dotted_lookup=False
     )
     assert settings.get('PARAMS') == {
         'PASSWORD': 'secret',
@@ -237,7 +238,7 @@ def test_dotted_traversal_access(settings):
     assert settings('PARAMS.DOTTED.KEY') is None
     assert settings('PARAMS').get('DOTTED.KEY') is True
 
-    settings.set('DOTTED.KEY', True)
+    settings.set('DOTTED.KEY', True, dotted_lookup=False)
     assert settings('DOTTED.KEY', dotted_lookup=False) is True
 
     settings.set(
@@ -256,3 +257,47 @@ def test_dotted_traversal_access(settings):
     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