Repository: rochacbruno/dynaconf
Pull request #40 (2018-05-03 18:00:58)

ini and json files + parseconf recursive and find_file function
- Added support for .ini and .json files
- parse conf is now recursive to parse dict inner data
- Cloned find_file function from dotenv

coveralls: 
[![Coverage Status](https://coveralls.io/builds/16822000/badge)](https://coveralls.io/builds/16822000)

Coverage increased (+1.4%) to 83.47% when pulling **7f98f3783ab6cf838f3119ac42e05b8849d4848c on inifiles** into **9ac6e433a6ed00869a86201596aecfd0b9e4af1d on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/16822000/badge)](https://coveralls.io/builds/16822000)

Coverage increased (+1.4%) to 83.47% when pulling **7f98f3783ab6cf838f3119ac42e05b8849d4848c on inifiles** into **9ac6e433a6ed00869a86201596aecfd0b9e4af1d on master**.


Patch:
diff --git a/.env b/.env
index ebd9354..e345cad 100644
--- a/.env
+++ b/.env
@@ -24,3 +24,6 @@ FLASK_INTVAR=@int 42
 FLASK_FLOATVAR=@float 4.2
 FLASK_BOOLVAR=@bool true
 FLASK_JSONVAR=@json ["flask", "rocks"]
+
+# test route used in jenkins
+FLASK_TEST_RULE='/dotenv'
diff --git a/.travis.yml b/.travis.yml
index dbbc240..bac15ff 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,9 +16,7 @@ env:
 before_script:
   - sleep 5
 python:
-  - "2.7"
   - "3.6"
-  - "pypy"
 install:
   - "pip install --upgrade pip"
   - "pip install --upgrade -r requirements_dev.txt"
diff --git a/HISTORY b/HISTORY
index 8706a97..fbf83b0 100644
--- a/HISTORY
+++ b/HISTORY
@@ -3,6 +3,12 @@ Changelog
 
 These are all the changes in Dynaconf since the first public release.
 
+0.5.6
+-----
+- 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
 -----
 - Implemented TOMl loader
diff --git a/Makefile b/Makefile
index da33f74..e2ed93c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,20 @@
 .PHONY: test install pep8 release clean doc
 
+test_examples:
+	@cd example/;python full_example.py
+	@cd example/app;python app.py
+	@cd example/app_with_dotenv;python app.py
+	@cd example/multiple_sources;python app.py
+	@cd example/toml_example/settings_module/;python app.py
+	@cd example/yaml_example/settings_module/;python app.py
+	@cd example/yaml_example/yaml_as_extra_config/;python app.py
+	@cd example/flask_with_dotenv;flask routes
+	@cd example/flask_with_toml;flask routes
+	@cd example/flask_with_yaml;flask routes
+	# @cd example/validator/;python app.py
+
 test: pep8
-	py.test -v --cov=dynaconf -l --tb=short --maxfail=1 tests/
+	py.test -v --cov-config .coveragerc --cov=dynaconf -l --tb=short --maxfail=1 tests/
 
 install:
 	python setup.py develop
diff --git a/README.md b/README.md
index a93d615..28d27a0 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,9 @@
 [![MIT License](https://img.shields.io/badge/license-MIT-007EC7.svg?style=flat-square)](/LICENSE) [![PyPI](https://img.shields.io/pypi/v/dynaconf.svg)](https://pypi.python.org/pypi/dynaconf) [![PyPI](https://img.shields.io/pypi/pyversions/dynaconf.svg)]() [![Travis CI](http://img.shields.io/travis/rochacbruno/dynaconf.svg)](https://travis-ci.org/rochacbruno/dynaconf) [![Coverage Status](https://coveralls.io/repos/rochacbruno/dynaconf/badge.svg?branch=master&service=github)](https://coveralls.io/github/rochacbruno/dynaconf?branch=master) [![Codacy grade](https://img.shields.io/codacy/grade/5074f5d870a24ddea79def463453545b.svg)](https://www.codacy.com/app/rochacbruno/dynaconf/dashboard)
 
 
-**dynaconf** a layered configuration system for Python applications 
-(with strong support for [12-factor applications](https://12factor.net/config)).
+**dynaconf** a layered configuration system for Python applications - 
+with strong support for [12-factor applications](https://12factor.net/config) 
+and **Flask** `app.config` extension.
 
 <br><br>
 
@@ -34,7 +35,9 @@ Connect(user=settings('USERNAME', 'admin'), passwd=settings('PASSWD', 1234))
 
 ##  Where the values come from? 
 
-- `settings.{py|yaml|toml|ini}` file
+> Dynaconf will look for variables in the following order (by default) and you can also customize the order of loaders.
+
+- Settings files files in the order: `settings.{py|yaml|toml|ini|json}` 
 - `.env` file
 - `export`ed Environment Variables
 - Remote storage servers
@@ -230,6 +233,62 @@ Then it will be applied using env var `DYNACONF_NAMESPACE` or context manager.
 > HINT: When using toml namespace identifier and first level vars are case
 > insensitive, dynaconf will always have them read as upper case.
 
+
+### using INIFILES
+
+> you need to install configobj `pip install configobj`
+
+Just save a `settings.ini` in the root dir.
+
+Using INI is easier because it support multiple namespace in a single file.
+
+Lets say you have `DYNACONF_NAMESPACE=DYNACONF` (the default)
+
+```ini
+[DYNACONF]
+VARIABLE = "this variable is available on every namespace"
+
+[DEVELOPMENT]
+HOST = "devserver.com"
+
+[production]
+host = "prodserver.com"
+```
+
+Then it will be applied using env var `DYNACONF_NAMESPACE` or context manager.
+
+> HINT: When using INI namespace identifier and first level vars are case
+> insensitive, dynaconf will always have them read as upper case.
+
+
+### using JSON
+
+Just save a `settings.json` in the root dir.
+
+Using JSON is easier because it support multiple namespace in a single file.
+
+Lets say you have `DYNACONF_NAMESPACE=DYNACONF` (the default)
+```json
+{
+  "DYNACONF": {
+    "VARIABLE": "this variable is available on every namespace",
+    "HOST": null
+  },
+  "DEVELOPMENT": {
+    "HOST": "devserver.com"
+  },
+  "production": {
+    "host": "prodserver.com"
+  }
+}
+```
+
+Then it will be applied using env var `DYNACONF_NAMESPACE` or context manager.
+
+> HINT: When using json namespace identifier and first level vars are case
+> insensitive, dynaconf will always have them read as upper case.
+
+
 # casting values from envvars
 
 Sometimes you need to set some values as specific types, boolean, integer, float or lists and dicts.
diff --git a/dynaconf/constants.py b/dynaconf/constants.py
new file mode 100644
index 0000000..50bb0bf
--- /dev/null
+++ b/dynaconf/constants.py
@@ -0,0 +1,5 @@
+# pragma: no cover
+INI_EXTENSIONS = ('.ini', '.conf', '.properties',)
+TOML_EXTENSIONS = ('.toml', '.tml',)
+YAML_EXTENSIONS = ('.yaml', '.yml',)
+JSON_EXTENSIONS = ('.json',)
diff --git a/dynaconf/contrib/flask_dynaconf.py b/dynaconf/contrib/flask_dynaconf.py
index 3a21dfe..5c69a8a 100644
--- a/dynaconf/contrib/flask_dynaconf.py
+++ b/dynaconf/contrib/flask_dynaconf.py
@@ -99,7 +99,7 @@ class FlaskDynaconf(object):
 
     def make_config(self, app):
         root_path = app.root_path
-        if self.instance_relative_config:
+        if self.instance_relative_config:  # pragma: no cover
             root_path = app.instance_path
         if self.dynaconf_instance:
             self.settings.update(self.kwargs)
diff --git a/dynaconf/default_settings.py b/dynaconf/default_settings.py
index d9e648f..27b84e1 100644
--- a/dynaconf/default_settings.py
+++ b/dynaconf/default_settings.py
@@ -3,7 +3,13 @@
 PROJECT_ROOT = "."
 
 # Default settings file
-SETTINGS_MODULE_FOR_DYNACONF = 'settings.py,settings.yaml,settings.toml'
+SETTINGS_MODULE_FOR_DYNACONF = (
+    'settings.py,'
+    'settings.yaml,settings.yml,'
+    'settings.toml,settings.tml,'
+    'settings.ini,settings.conf,settings.properties,'
+    'settings.json'
+)
 
 # Namespace for envvars
 DYNACONF_NAMESPACE = 'DYNACONF'
diff --git a/dynaconf/loaders/__init__.py b/dynaconf/loaders/__init__.py
index af2fb73..1b2c407 100644
--- a/dynaconf/loaders/__init__.py
+++ b/dynaconf/loaders/__init__.py
@@ -1,7 +1,10 @@
 import os
 import importlib
+from dynaconf import constants as ct
 from dynaconf import default_settings
-from dynaconf.loaders import yaml_loader, env_loader, toml_loader
+from dynaconf.loaders import (
+    yaml_loader, env_loader, toml_loader, json_loader, ini_loader
+)
 
 
 def default_loader(obj):
@@ -30,7 +33,8 @@ def module_loader(obj, settings_module=None, namespace=None, silent=False):
     for mod_file in files:
         # can be set to multiple files settings.py,settings.yaml,...
 
-        if mod_file.endswith(('.yaml', '.yml')):
+        # NOTE: all this cascade can be refactored to use a loader class!
+        if mod_file.endswith(ct.YAML_EXTENSIONS):
             obj.logger.info("Trying to load YAML {}".format(mod_file))
             yaml_loader.load(
                 obj,
@@ -40,7 +44,8 @@ def module_loader(obj, settings_module=None, namespace=None, silent=False):
             )
             continue
 
-        if mod_file.endswith(('.toml',)):
+        if mod_file.endswith(ct.TOML_EXTENSIONS):
+            obj.logger.info("Trying to load TOML {}".format(mod_file))
             toml_loader.load(
                 obj,
                 filename=mod_file,
@@ -49,6 +54,28 @@ def module_loader(obj, settings_module=None, namespace=None, silent=False):
             )
             continue
 
+        if mod_file.endswith(ct.INI_EXTENSIONS):
+            obj.logger.info("Trying to load INI {}".format(mod_file))
+            ini_loader.load(
+                obj,
+                filename=mod_file,
+                namespace=namespace,
+                silent=silent
+            )
+            continue
+
+        if mod_file.endswith(ct.JSON_EXTENSIONS):
+            obj.logger.info("Trying to load JSON {}".format(mod_file))
+            json_loader.load(
+                obj,
+                filename=mod_file,
+                namespace=namespace,
+                silent=silent
+            )
+            continue
+
+        obj.logger.info("Trying to load Python module {}".format(mod_file))
+
         # load from default defined module if exists (never gets cleaned)
         load_from_module(obj, mod_file)
 
@@ -86,6 +113,9 @@ def load_from_module(obj, settings_module,
         mod = obj.import_from_filename(settings_module, silent=silent)
         loaded_from = 'filename'
 
+    obj.logger.info(
+        "Module {} Loaded from {}".format(settings_module, loaded_from))
+
     for setting in dir(mod):
         if setting.isupper():
             setting_value = getattr(mod, setting)
diff --git a/dynaconf/loaders/ini_loader.py b/dynaconf/loaders/ini_loader.py
new file mode 100644
index 0000000..ca83398
--- /dev/null
+++ b/dynaconf/loaders/ini_loader.py
@@ -0,0 +1,104 @@
+# coding: utf-8
+from dynaconf.constants import INI_EXTENSIONS
+try:
+    from configobj import ConfigObj
+except ImportError as e:  # pragma: no cover
+    ConfigObj = None
+
+from dynaconf.utils.files import find_file
+
+IDENTIFIER = 'ini_loader'
+
+
+def load(obj, namespace=None, silent=True, key=None, filename=None):
+    """
+    Reads and loads in to "settings" a single key or all keys from ini file
+    :param obj: the settings instance
+    :param namespace: settings namespace default='DYNACONF'
+    :param silent: if errors should raise
+    :param key: if defined load a single key, else load all in namespace
+    :return: None
+    """
+    if ConfigObj is None:  # pragma: no cover
+        obj.logger.warning(
+            "configobj package is not installed in your environment.\n"
+            "To use this loader you have to install it with\n"
+            "pip install configobj\n"
+            "or\n"
+            "pip install dynaconf[ini]"
+        )
+        return
+
+    filename = filename or obj.get('INI')
+    if not filename:
+        return
+
+    namespace = namespace or obj.get('DYNACONF_NAMESPACE')
+
+    # clean(obj, namespace, identifier=filename)
+
+    # can be a filename settings.yml
+    # can be a multiple fileset settings1.ini, settings2.ini etc
+    # and also a list of strings ['aaa:a', 'bbb:c']
+    # and can also be a single string 'aa:a'
+    if not isinstance(filename, (list, tuple)):
+        split_files = filename.split(',')
+        if all([f.endswith(INI_EXTENSIONS) for f in split_files]):  # noqa
+            files = split_files  # it is a ['file.ini', ...]
+        else:  # it is a single ini string
+            files = [filename]
+    else:  # it is already a list/tuple
+        files = filename
+
+    for ini_file in files:
+        if ini_file.endswith(INI_EXTENSIONS):  # pragma: no cover
+            obj.logger.debug('Trying to load ini {}'.format(ini_file))
+            try:
+                ini_data = ConfigObj(
+                    open(find_file(ini_file, usecwd=True))
+                ).dict()
+            except IOError as e:
+                obj.logger.warning(
+                    "Unable to load ini file {}".format(str(e)))
+                ini_data = None
+        else:
+            # for tests it is possible to pass ini string
+            ini_data = ConfigObj(ini_file.split('\n')).dict()
+
+        if not ini_data:
+            continue
+
+        ini_data = {key.lower(): value for key, value in ini_data.items()}
+
+        # ---->
+        # Load from namespace_filename.ini
+
+        data = {}
+        try:
+            data = ini_data[namespace.lower()]
+        except KeyError:
+            if silent:
+                obj.logger.debug(
+                    '%s namespace not defined in ini source' % namespace
+                )
+            else:
+                raise KeyError(
+                    '%s namespace not defined in %s' % (namespace, filename)
+                )
+
+        if namespace and namespace != obj.get('DYNACONF_NAMESPACE'):
+            identifier = "{0}_{1}".format(IDENTIFIER, namespace.lower())
+        else:
+            identifier = IDENTIFIER
+
+        if not key:
+            obj.update(data, loader_identifier=identifier)
+        else:
+            obj.set(key, data.get(key), loader_identifier=identifier)
+
+
+def clean(obj, namespace, silent=True):  # noqa
+    for identifier, data in obj.loaded_by_loaders.items():
+        if identifier.startswith('ini_loader_'):
+            for key in data:
+                obj.unset(key)
diff --git a/dynaconf/loaders/json_loader.py b/dynaconf/loaders/json_loader.py
new file mode 100644
index 0000000..06e6f3e
--- /dev/null
+++ b/dynaconf/loaders/json_loader.py
@@ -0,0 +1,90 @@
+# coding: utf-8
+from dynaconf.constants import JSON_EXTENSIONS
+import json
+from dynaconf.utils.files import find_file
+
+IDENTIFIER = 'json_loader'
+
+
+def load(obj, namespace=None, silent=True, key=None, filename=None):
+    """
+    Reads and loads in to "settings" a single key or all keys from json file
+    :param obj: the settings instance
+    :param namespace: settings namespace default='DYNACONF'
+    :param silent: if errors should raise
+    :param key: if defined load a single key, else load all in namespace
+    :return: None
+    """
+    filename = filename or obj.get('JSON')
+    if not filename:
+        return
+
+    namespace = namespace or obj.get('DYNACONF_NAMESPACE')
+
+    # clean(obj, namespace, identifier=filename)
+
+    # can be a filename settings.yml
+    # can be a multiple fileset settings1.json, settings2.json etc
+    # and also a list of strings ['aaa:a', 'bbb:c']
+    # and can also be a single string 'aa:a'
+    if not isinstance(filename, (list, tuple)):
+        split_files = filename.split(',')
+        if all([f.endswith(JSON_EXTENSIONS) for f in split_files]):  # noqa
+            files = split_files  # it is a ['file.json', ...]
+        else:  # it is a single json string
+            files = [filename]
+    else:  # it is already a list/tuple
+        files = filename
+
+    for json_file in files:
+        if json_file.endswith(JSON_EXTENSIONS):  # pragma: no cover
+            obj.logger.debug('Trying to load json {}'.format(json_file))
+            try:
+                json_data = json.load(
+                    open(find_file(json_file, usecwd=True))
+                )
+            except (IOError, json.decoder.JSONDecodeError) as e:
+                obj.logger.warning(
+                    "Unable to load json {} file {}".format(json_file, str(e)))
+                json_data = None
+        else:
+            # for tests it is possible to pass json string
+            json_data = json.loads(json_file)
+
+        if not json_data:
+            continue
+
+        json_data = {key.lower(): value for key, value in json_data.items()}
+
+        # ---->
+        # Load from namespace_filename.json
+
+        data = {}
+        try:
+            data = json_data[namespace.lower()]
+        except KeyError:
+            if silent:
+                obj.logger.debug(
+                    '%s namespace not defined in json source' % namespace
+                )
+            else:
+                raise KeyError(
+                    '%s namespace not defined in %s' % (namespace, filename)
+                )
+
+        if namespace and namespace != obj.get('DYNACONF_NAMESPACE'):
+            identifier = "{0}_{1}".format(IDENTIFIER, namespace.lower())
+        else:
+            identifier = IDENTIFIER
+
+        if not key:
+            obj.update(data, loader_identifier=identifier)
+        else:
+            obj.set(key, data.get(key), loader_identifier=identifier)
+
+
+def clean(obj, namespace, silent=True):  # noqa
+    for identifier, data in obj.loaded_by_loaders.items():
+        if identifier.startswith('json_loader_'):
+            for key in data:
+                obj.unset(key)
diff --git a/dynaconf/loaders/toml_loader.py b/dynaconf/loaders/toml_loader.py
index 933f436..4b935ef 100644
--- a/dynaconf/loaders/toml_loader.py
+++ b/dynaconf/loaders/toml_loader.py
@@ -1,10 +1,11 @@
 # coding: utf-8
+from dynaconf.constants import TOML_EXTENSIONS
 try:
     import toml
 except ImportError as e:  # pragma: no cover
     toml = None
 
-from dotenv import find_dotenv
+from dynaconf.utils.files import find_file
 
 IDENTIFIER = 'toml_loader'
 
@@ -42,7 +43,7 @@ def load(obj, namespace=None, silent=True, key=None, filename=None):
     # and can also be a single string 'aa:a'
     if not isinstance(filename, (list, tuple)):
         split_files = filename.split(',')
-        if all([f.endswith(('.toml', '.tml')) for f in split_files]):
+        if all([f.endswith(TOML_EXTENSIONS) for f in split_files]):
             files = split_files  # it is a ['file.toml', ...]
         else:  # it is a single TOML string
             files = [filename]
@@ -50,11 +51,11 @@ def load(obj, namespace=None, silent=True, key=None, filename=None):
         files = filename
 
     for toml_file in files:
-        if toml_file.endswith(('.toml', '.tml')):  # pragma: no cover
+        if toml_file.endswith(TOML_EXTENSIONS):  # pragma: no cover
             obj.logger.debug('Trying to load TOML {}'.format(toml_file))
             try:
                 toml_data = toml.load(
-                    open(find_dotenv(toml_file, usecwd=True))
+                    open(find_file(toml_file, usecwd=True))
                 )
             except IOError as e:
                 obj.logger.warning(
diff --git a/dynaconf/loaders/yaml_loader.py b/dynaconf/loaders/yaml_loader.py
index 6e626d2..0f609ef 100644
--- a/dynaconf/loaders/yaml_loader.py
+++ b/dynaconf/loaders/yaml_loader.py
@@ -1,10 +1,11 @@
 # coding: utf-8
+from dynaconf.constants import YAML_EXTENSIONS
 try:
     import yaml
 except ImportError as e:  # pragma: no cover
     yaml = None
 
-from dotenv import find_dotenv
+from dynaconf.utils.files import find_file
 
 IDENTIFIER = 'yaml_loader'
 
@@ -42,7 +43,7 @@ def load(obj, namespace=None, silent=True, key=None, filename=None):
     # and can also be a single string 'aa:a'
     if not isinstance(filename, (list, tuple)):
         split_files = filename.split(',')
-        if all([f.endswith(('.yaml', '.yml')) for f in split_files]):
+        if all([f.endswith(YAML_EXTENSIONS) for f in split_files]):
             files = split_files  # it is a ['file.yml', ...]
         else:  # it is a single YAML string
             files = [filename]
@@ -50,10 +51,10 @@ def load(obj, namespace=None, silent=True, key=None, filename=None):
         files = filename
 
     for yaml_file in files:
-        if yaml_file.endswith(('.yaml', '.yml')):  # pragma: no cover
+        if yaml_file.endswith(YAML_EXTENSIONS):  # pragma: no cover
             try:
                 yaml_data = yaml.load(
-                    open(find_dotenv(yaml_file, usecwd=True))
+                    open(find_file(yaml_file, usecwd=True))
                 )
             except IOError as e:
                 obj.logger.warning(
diff --git a/dynaconf/transformator.py b/dynaconf/transformator.py
index 5ae40d1..2e75532 100644
--- a/dynaconf/transformator.py
+++ b/dynaconf/transformator.py
@@ -1,4 +1,5 @@
 # coding: utf-8
+# pragma: no cover
 
 
 class Transformator:
diff --git a/dynaconf/utils/files.py b/dynaconf/utils/files.py
new file mode 100644
index 0000000..da6b623
--- /dev/null
+++ b/dynaconf/utils/files.py
@@ -0,0 +1,44 @@
+import os
+import sys
+
+
+def _walk_to_root(path):
+    """
+    Yield directories starting from the given directory up to the root
+    """
+    if not os.path.exists(path):  # pragma: no cover
+        raise IOError('Starting path not found')
+
+    if os.path.isfile(path):  # pragma: no cover
+        path = os.path.dirname(path)
+
+    last_dir = None
+    current_dir = os.path.abspath(path)
+    while last_dir != current_dir:
+        yield current_dir
+        parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))
+        last_dir, current_dir = current_dir, parent_dir
+
+
+def find_file(filename='.env', raise_error_if_not_found=False, usecwd=True):
+    """
+    Search in increasingly higher folders for the given file
+    Returns path to the file if found, or an empty string otherwise
+    """
+    if usecwd or '__file__' not in globals():
+        # should work without __file__, e.g. in REPL or IPython notebook
+        path = os.getcwd()
+    else:  # pragma: no cover
+        # will work for .py files
+        frame_filename = sys._getframe().f_back.f_code.co_filename
+        path = os.path.dirname(os.path.abspath(frame_filename))
+
+    for dirname in _walk_to_root(path):
+        check_path = os.path.join(dirname, filename)
+        if os.path.exists(check_path):
+            return check_path
+
+    if raise_error_if_not_found:
+        raise IOError('File not found')
+
+    return ''
diff --git a/dynaconf/utils/parse_conf.py b/dynaconf/utils/parse_conf.py
index 89fc5fd..ff1f1b3 100644
--- a/dynaconf/utils/parse_conf.py
+++ b/dynaconf/utils/parse_conf.py
@@ -1,6 +1,7 @@
 import json
 
 from six import string_types
+from dynaconf.utils.boxing import DynaBox
 
 true_values = ('t', 'true', 'enabled', '1', 'on', 'yes')
 converters = {
@@ -19,7 +20,7 @@ converters = {
 }
 
 
-def parse_conf_data(data):
+def _parse_conf_data(data):
     """
     @int @bool @float @json (for lists and dicts)
     strings does not need converters
@@ -41,6 +42,21 @@ def parse_conf_data(data):
     return data
 
 
+def parse_conf_data(data):
+    if isinstance(data, (tuple, list)):
+        # recursively parse each sequence item
+        return [parse_conf_data(item) for item in data]
+    elif isinstance(data, (dict, DynaBox)):
+        # recursively parse inner dict items
+        _parsed = {}
+        for k, v in data.items():
+            _parsed[k] = parse_conf_data(v)
+        return _parsed
+    else:
+        # return parsed string value
+        return _parse_conf_data(data)
+
+
 def unparse_conf_data(value):
     if isinstance(value, bool):
         return "@bool %s" % value
diff --git a/dynaconf/validator.py b/dynaconf/validator.py
index b7b1d50..9c9b2d3 100644
--- a/dynaconf/validator.py
+++ b/dynaconf/validator.py
@@ -1,4 +1,5 @@
 # coding: utf-8
+# pragma: no cover
 from dynaconf import validator_conditions
 
 
@@ -57,7 +58,7 @@ class Validator(object):
     """
     def __init__(
             self,
-            names,
+            *names,
             must_exist=None,
             condition=None,
             when=None,
diff --git a/dynaconf/validator_conditions.py b/dynaconf/validator_conditions.py
index febdfe5..52f430d 100644
--- a/dynaconf/validator_conditions.py
+++ b/dynaconf/validator_conditions.py
@@ -1,4 +1,5 @@
 # coding: utf-8
+# pragma: no cover
 """
 Implement basic assertions to be used in assertion action
 """
diff --git a/example/app/env.sh b/example/app/env.sh
deleted file mode 100644
index fad90eb..0000000
--- a/example/app/env.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env bash
-export EXAMPLE_SETTINGS_MODULE=mysettings
-export EXAMPLE_MYSQL_PASSWD=SuperSecret
diff --git a/example/flask_with_dotenv/app.py b/example/flask_with_dotenv/app.py
index 0764eb0..2f5e2aa 100644
--- a/example/flask_with_dotenv/app.py
+++ b/example/flask_with_dotenv/app.py
@@ -18,3 +18,6 @@ def index():
 @app.route('/test')
 def test():
     return app.config['HELLO']
+
+
+app.add_url_rule(app.config.TEST_RULE, view_func=lambda: 'test')
diff --git a/example/flask_with_toml/app.py b/example/flask_with_toml/app.py
index 963f6b8..186d68e 100644
--- a/example/flask_with_toml/app.py
+++ b/example/flask_with_toml/app.py
@@ -21,5 +21,8 @@ def index():
     return render_template('dynaconf.html')
 
 
+app.add_url_rule(app.config.TEST_RULE, view_func=lambda: 'test')
+
+
 if __name__ == '__main__':
     app.run()
diff --git a/example/flask_with_toml/settings.toml b/example/flask_with_toml/settings.toml
index c6b0fec..4537dea 100644
--- a/example/flask_with_toml/settings.toml
+++ b/example/flask_with_toml/settings.toml
@@ -4,4 +4,5 @@ DEBUG = true
 SERVER = "flaskdynaconf.com"
 PORT = 6666.0
 MESSAGE = "Dynaconf works like a charm with Flask and TOML"
+TEST_RULE='/toml'
 
diff --git a/example/flask_with_yaml/app.py b/example/flask_with_yaml/app.py
index e19e4c7..8642d4c 100644
--- a/example/flask_with_yaml/app.py
+++ b/example/flask_with_yaml/app.py
@@ -21,5 +21,8 @@ def index():
     return render_template('dynaconf.html')
 
 
+app.add_url_rule(app.config.TEST_RULE, view_func=lambda: 'test')
+
+
 if __name__ == '__main__':
     app.run()
diff --git a/example/flask_with_yaml/settings.yml b/example/flask_with_yaml/settings.yml
index 02c59d0..dddd196 100644
--- a/example/flask_with_yaml/settings.yml
+++ b/example/flask_with_yaml/settings.yml
@@ -6,4 +6,5 @@ MYSITE:
   SERVER: flaskdynaconf.com
   PORT: 6666
   MESSAGE: Dynaconf works like a charm with Flask and YAML
+  TEST_RULE: '/yaml'
 
diff --git a/example/multiple_sources/app.py b/example/multiple_sources/app.py
new file mode 100644
index 0000000..647ca67
--- /dev/null
+++ b/example/multiple_sources/app.py
@@ -0,0 +1,12 @@
+from dynaconf import settings
+
+print("Read from settings.py:", settings.PYTHON_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
diff --git a/example/multiple_sources/settings.conf b/example/multiple_sources/settings.conf
new file mode 100644
index 0000000..3598d6e
--- /dev/null
+++ b/example/multiple_sources/settings.conf
@@ -0,0 +1,2 @@
+[dynaconf]
+conf_var=1
diff --git a/example/multiple_sources/settings.ini b/example/multiple_sources/settings.ini
new file mode 100644
index 0000000..142b2da
--- /dev/null
+++ b/example/multiple_sources/settings.ini
@@ -0,0 +1,2 @@
+[dynaconf]
+ini_var=1
diff --git a/example/multiple_sources/settings.json b/example/multiple_sources/settings.json
new file mode 100644
index 0000000..d0211f1
--- /dev/null
+++ b/example/multiple_sources/settings.json
@@ -0,0 +1,1 @@
+{"dynaconf": {"json_var": 1}}
diff --git a/example/multiple_sources/settings.properties b/example/multiple_sources/settings.properties
new file mode 100644
index 0000000..0ed1a65
--- /dev/null
+++ b/example/multiple_sources/settings.properties
@@ -0,0 +1,2 @@
+[dynaconf]
+properties_var=1
diff --git a/example/multiple_sources/settings.py b/example/multiple_sources/settings.py
new file mode 100644
index 0000000..e47ea2d
--- /dev/null
+++ b/example/multiple_sources/settings.py
@@ -0,0 +1,1 @@
+PYTHON_VAR = 1
diff --git a/example/multiple_sources/settings.tml b/example/multiple_sources/settings.tml
new file mode 100644
index 0000000..2ff76d7
--- /dev/null
+++ b/example/multiple_sources/settings.tml
@@ -0,0 +1,2 @@
+[dynaconf]
+tml_var=1
diff --git a/example/multiple_sources/settings.toml b/example/multiple_sources/settings.toml
new file mode 100644
index 0000000..37a7df5
--- /dev/null
+++ b/example/multiple_sources/settings.toml
@@ -0,0 +1,2 @@
+[dynaconf]
+toml_var=1
diff --git a/example/multiple_sources/settings.yaml b/example/multiple_sources/settings.yaml
new file mode 100644
index 0000000..986e090
--- /dev/null
+++ b/example/multiple_sources/settings.yaml
@@ -0,0 +1,2 @@
+dynaconf:
+  yaml_var: 1
diff --git a/example/multiple_sources/settings.yml b/example/multiple_sources/settings.yml
new file mode 100644
index 0000000..2347aa3
--- /dev/null
+++ b/example/multiple_sources/settings.yml
@@ -0,0 +1,2 @@
+dynaconf:
+  yml_var: 1
diff --git a/example/validator/app.py b/example/validator/app.py
index 5cf68cc..56be988 100644
--- a/example/validator/app.py
+++ b/example/validator/app.py
@@ -6,4 +6,4 @@ print(settings.MYSQL_HOST)  # noqa
 print(settings.MYSQL_PASSWD)  # noqa
 print(settings.EXAMPLE)  # noqa
 
-print(settings.current_namespace)
+print(settings.current_namespace)  # noqa
diff --git a/example/validator/env.sh b/example/validator/env.sh
deleted file mode 100644
index d562f6f..0000000
--- a/example/validator/env.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env bash
-export EXAMPLE_SETTINGS_MODULE=settings
-export EXAMPLE_MYSQL_PASSWD=SuperSecret
diff --git a/example/yaml_example/yaml_as_extra_config/env.sh b/example/yaml_example/yaml_as_extra_config/env.sh
deleted file mode 100644
index 6cf1ad1..0000000
--- a/example/yaml_example/yaml_as_extra_config/env.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env bash
-# optional if not defined in settings.py
-export DYNACONF_YAML=extra_settings.yaml
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 1cc781d..d53eeb8 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -9,6 +9,7 @@ flask>=1.0
 python-box
 python-dotenv
 setuptools>=38.6.0
+configobj
 
 # testing
 coveralls
diff --git a/setup.py b/setup.py
index 81e7e31..b90fd36 100644
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,7 @@ def read(*names, **kwargs):
 
 setup(
     name='dynaconf',
-    version="0.5.5",
+    version="0.5.6",
     url='https://github.com/rochacbruno/dynaconf',
     license='MIT',
     author="Bruno Rocha",
@@ -47,7 +47,9 @@ setup(
         "redis": ['redis'],
         "yaml": ['PyYAML'],
         "toml": ['toml'],
-        "all": ['redis', 'PyYAML', 'toml'],
+        "ini": ['configobj'],
+        "configobj": ['configobj'],
+        "all": ['redis', 'PyYAML', 'toml', 'configobj'],
     },
     setup_requires=['setuptools>=38.6.0'],
     classifiers=[
diff --git a/tox.ini b/tox.ini
index abbff28..dfff223 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27, py36
+envlist = py36
 
 [testenv]
 deps = -rrequirements_dev.txt


Test patch:
diff --git a/dynaconf/test_settings.py b/dynaconf/test_settings.py
index af732cc..ab15ff2 100644
--- a/dynaconf/test_settings.py
+++ b/dynaconf/test_settings.py
@@ -1,3 +1,4 @@
+# pragma: no cover
 TESTING = True
 LOADERS_FOR_DYNACONF = [
     'dynaconf.loaders.env_loader',
diff --git a/tests/test_env_loader.py b/tests/test_env_loader.py
index bca4654..d3fb58f 100644
--- a/tests/test_env_loader.py
+++ b/tests/test_env_loader.py
@@ -5,8 +5,8 @@ from dynaconf import settings  # noqa
 os.environ['DYNACONF_HOSTNAME'] = 'host.com'
 os.environ['DYNACONF_PORT'] = '@int 5000'
 os.environ['DYNACONF_VALUE'] = '@float 42.1'
-os.environ['DYNACONF_ALIST'] = '@json ["item1", "item2", "item3"]'
-os.environ['DYNACONF_ADICT'] = '@json {"key": "value"}'
+os.environ['DYNACONF_ALIST'] = '@json ["item1", "item2", "item3", 123]'
+os.environ['DYNACONF_ADICT'] = '@json {"key": "value", "int": 42}'
 os.environ['DYNACONF_DEBUG'] = '@bool true'
 os.environ['PROJECT1_HOSTNAME'] = 'otherhost.com'
 os.environ['PROJECT1_PORT'] = '@int 8080'
@@ -16,6 +16,8 @@ settings.configure()
 
 def test_env_loader():
     assert settings.HOSTNAME == 'host.com'
+    assert settings.ALIST == ["item1", "item2", "item3", 123]
+    assert settings.ADICT == {"key": "value", "int": 42}
 
 
 def test_single_key():
diff --git a/tests/test_ini_loader.py b/tests/test_ini_loader.py
new file mode 100644
index 0000000..28e34cf
--- /dev/null
+++ b/tests/test_ini_loader.py
@@ -0,0 +1,102 @@
+import pytest
+from dynaconf import LazySettings
+from dynaconf.loaders.ini_loader import load
+
+settings = LazySettings(
+    DYNACONF_NAMESPACE='EXAMPLE',
+)
+
+
+INI = """
+[example]
+password = '@int 99999'
+host = "server.com"
+port = '@int 8080'
+alist = item1, item2, '@int 23'
+
+  [[service]]
+  url = "service.com"
+  port = '@int 80'
+
+    [[[auth]]]
+    password = "qwerty"
+    test = '@int 1234'
+
+[development]
+password = '@int 88888'
+host = "dev_server.com"
+"""
+
+INI2 = """
+[example]
+secret = "@float 42"
+password = '@int 123456'
+"""
+
+INIS = [INI, INI2]
+
+
+def test_load_from_ini():
+    """Assert loads from INI string"""
+    load(settings, filename=INI)
+    assert settings.HOST == 'server.com'
+    assert settings.PORT == 8080
+    assert settings.ALIST == ['item1', 'item2', 23]
+    assert settings.SERVICE['url'] == 'service.com'
+    assert settings.SERVICE.url == 'service.com'
+    assert settings.SERVICE.port == 80
+    assert settings.SERVICE.auth.password == 'qwerty'
+    assert settings.SERVICE.auth.test == 1234
+    load(settings, filename=INI, namespace='DEVELOPMENT')
+    assert settings.HOST == 'dev_server.com'
+    load(settings, filename=INI)
+    assert settings.HOST == 'server.com'
+
+
+def test_load_from_multiple_ini():
+    """Assert loads from INI string"""
+    load(settings, filename=INIS)
+    assert settings.HOST == 'server.com'
+    assert settings.PASSWORD == 123456
+    assert settings.SECRET == 42.0
+    assert settings.PORT == 8080
+    assert settings.SERVICE['url'] == 'service.com'
+    assert settings.SERVICE.url == 'service.com'
+    assert settings.SERVICE.port == 80
+    assert settings.SERVICE.auth.password == 'qwerty'
+    assert settings.SERVICE.auth.test == 1234
+    load(settings, filename=INIS, namespace='DEVELOPMENT')
+    assert settings.HOST == 'dev_server.com'
+    assert settings.PASSWORD == 88888
+    load(settings, filename=INIS)
+    assert settings.HOST == 'server.com'
+    assert settings.PASSWORD == 123456
+
+
+def test_no_filename_is_none():
+    """Assert if passed no filename return is None"""
+    assert load(settings) is None
+
+
+def test_key_error_on_invalid_namespace():
+    """Assert error raised if namespace is not found in INI"""
+    with pytest.raises(KeyError):
+        load(settings, filename=INI, namespace='FOOBAR', silent=False)
+
+
+def test_no_key_error_on_invalid_namespace():
+    """Assert error raised if namespace is not found in INI"""
+    load(settings, filename=INI, namespace='FOOBAR', silent=True)
+
+
+def test_load_single_key():
+    """Test loading a single key"""
+    ini = """
+    [foo]
+    bar = "blaz"
+    zaz = "naz"
+    """
+    load(settings, filename=ini, namespace='FOO', key='bar')
+    assert settings.BAR == 'blaz'
+    assert settings.exists('BAR') is True
+    assert settings.exists('ZAZ') is False
diff --git a/tests/test_json_loader.py b/tests/test_json_loader.py
new file mode 100644
index 0000000..3e87e30
--- /dev/null
+++ b/tests/test_json_loader.py
@@ -0,0 +1,111 @@
+import pytest
+from dynaconf import LazySettings
+from dynaconf.loaders.json_loader import load
+
+settings = LazySettings(
+    DYNACONF_NAMESPACE='EXAMPLE',
+)
+
+
+JSON = """
+{
+  "a": "a,b",
+  "example": {
+    "password": 99999,
+    "host": "server.com",
+    "port": 8080,
+    "service": {
+      "url": "service.com",
+      "port": 80,
+      "auth": {
+        "password": "qwerty",
+        "test": 1234
+      }
+    }
+  },
+  "development": {
+    "password": 88888,
+    "host": "dev_server.com"
+  }
+}
+"""
+
+# the @float is not needed in JSON but kept to ensure it works
+JSON2 = """
+{
+  "example": {
+    "secret": "@float 42",
+    "password": 123456
+  }
+}
+"""
+
+JSONS = [JSON, JSON2]
+
+
+def test_load_from_json():
+    """Assert loads from JSON string"""
+    load(settings, filename=JSON)
+    assert settings.HOST == 'server.com'
+    assert settings.PORT == 8080
+    assert settings.SERVICE['url'] == 'service.com'
+    assert settings.SERVICE.url == 'service.com'
+    assert settings.SERVICE.port == 80
+    assert settings.SERVICE.auth.password == 'qwerty'
+    assert settings.SERVICE.auth.test == 1234
+    load(settings, filename=JSON, namespace='DEVELOPMENT')
+    assert settings.HOST == 'dev_server.com'
+    load(settings, filename=JSON)
+    assert settings.HOST == 'server.com'
+
+
+def test_load_from_multiple_json():
+    """Assert loads from JSON string"""
+    load(settings, filename=JSONS)
+    assert settings.HOST == 'server.com'
+    assert settings.PASSWORD == 123456
+    assert settings.SECRET == 42.0
+    assert settings.PORT == 8080
+    assert settings.SERVICE['url'] == 'service.com'
+    assert settings.SERVICE.url == 'service.com'
+    assert settings.SERVICE.port == 80
+    assert settings.SERVICE.auth.password == 'qwerty'
+    assert settings.SERVICE.auth.test == 1234
+    load(settings, filename=JSONS, namespace='DEVELOPMENT')
+    assert settings.HOST == 'dev_server.com'
+    assert settings.PASSWORD == 88888
+    load(settings, filename=JSONS)
+    assert settings.HOST == 'server.com'
+    assert settings.PASSWORD == 123456
+
+
+def test_no_filename_is_none():
+    """Assert if passed no filename return is None"""
+    assert load(settings) is None
+
+
+def test_key_error_on_invalid_namespace():
+    """Assert error raised if namespace is not found in JSON"""
+    with pytest.raises(KeyError):
+        load(settings, filename=JSON, namespace='FOOBAR', silent=False)
+
+
+def test_no_key_error_on_invalid_namespace():
+    """Assert error raised if namespace is not found in JSON"""
+    load(settings, filename=JSON, namespace='FOOBAR', silent=True)
+
+
+def test_load_single_key():
+    """Test loading a single key"""
+    _JSON = """
+    {
+      "foo": {
+        "bar": "blaz",
+        "zaz": "naz"
+      }
+    }
+    """
+    load(settings, filename=_JSON, namespace='FOO', key='bar')
+    assert settings.BAR == 'blaz'
+    assert settings.exists('BAR') is True
+    assert settings.exists('ZAZ') is False
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 3ef02c2..6fa5da5 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,6 +1,9 @@
 # coding: utf-8
-
+import os
+import pytest
+import tempfile
 from dynaconf.utils.parse_conf import unparse_conf_data, parse_conf_data
+from dynaconf.utils.files import find_file
 
 
 def test_unparse():
@@ -19,3 +22,42 @@ def test_cast_bool(settings):
     """Covers https://github.com/rochacbruno/dynaconf/issues/14"""
     assert parse_conf_data(False) is False
     assert settings.get('SIMPLE_BOOL', cast='@bool') is False
+
+
+def test_find_file():
+    """
+    Create a temporary folder structure like the following:
+        tmpXiWxa5/
+        └── child1
+            ├── child2
+            │   └── child3
+            │       └── child4
+            └── .env
+    Then try to automatically `find_dotenv` starting in `child4`
+    """
+    tmpdir = os.path.realpath(tempfile.mkdtemp())
+
+    curr_dir = tmpdir
+    dirs = []
+    for f in ['child1', 'child2', 'child3', 'child4']:
+        curr_dir = os.path.join(curr_dir, f)
+        dirs.append(curr_dir)
+        os.mkdir(curr_dir)
+
+    child1, child4 = dirs[0], dirs[-1]
+
+    # change the working directory for testing
+    os.chdir(child4)
+
+    # try without a .env file and force error
+    with pytest.raises(IOError):
+        find_file(raise_error_if_not_found=True, usecwd=True)
+
+    # try without a .env file and fail silently
+    assert find_file(usecwd=True) == ''
+
+    # now place a .env file a few levels up and make sure it's found
+    filename = os.path.join(child1, '.env')
+    with open(filename, 'w') as f:
+        f.write("TEST=test\n")
+    assert find_file(usecwd=True) == filename