Repository: rochacbruno/dynaconf
Pull request #13 (2017-02-12 06:06:56)

Added YAML support


Patch:
diff --git a/.coveragerc b/.coveragerc
index 565a5ac..84cd899 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -9,3 +9,5 @@ omit =
     dynaconf/test_settings.py
     dynaconf/utils/functional.py
     dynaconf/utils/redis_writer.py
+    dynaconf/loaders/redis_loader.py
+    dynaconf/loaders/__init__.py
diff --git a/dynaconf/base.py b/dynaconf/base.py
index 9268249..b4d1257 100644
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -10,6 +10,7 @@ from six import string_types
 
 from dynaconf import default_settings
 from dynaconf.loaders import default_loader, module_loader
+from dynaconf.loaders import yaml_loader
 from dynaconf.utils.functional import LazyObject, empty
 from dynaconf.utils.parse_conf import converters, parse_conf_data
 
@@ -69,7 +70,10 @@ class LazySettings(LazyObject):
         if self._wrapped is empty:
             self._setup()
         if name in self._wrapped._deleted:  # noqa
-            raise AttributeError("Attribute %s was deleted" % name)
+            raise AttributeError(
+                "Attribute %s was deleted, "
+                "or belongs to different namespace" % name
+            )
         always_fresh = self._wrapped.DYNACONF_ALWAYS_FRESH_VARS
         if self._wrapped._fresh or name in always_fresh:  # noqa
             return self._wrapped.get_fresh(name)
@@ -415,7 +419,11 @@ class Settings(object):
 
     def execute_loaders(self, namespace=None, silent=None, key=None):
         default_loader(self)
-        module_loader(self)
+        module_loader(self, namespace=namespace)
+        if self.exists('YAML'):
+            yaml_loader.load(
+                self, namespace=namespace, filename=self.get('YAML')
+            )
         silent = silent or self.DYNACONF_SILENT_ERRORS
         for loader in self.loaders:
             loader.load(self, namespace, silent=silent, key=key)
diff --git a/dynaconf/default_settings.py b/dynaconf/default_settings.py
index b37af38..d9f7c7d 100644
--- a/dynaconf/default_settings.py
+++ b/dynaconf/default_settings.py
@@ -18,7 +18,7 @@ REDIS_FOR_DYNACONF = {
     'db': 0
 }
 
-# Loaders to read namespace based vars from diferent data stores
+# Loaders to read namespace based vars from different data stores
 LOADERS_FOR_DYNACONF = [
     'dynaconf.loaders.env_loader',
     # 'dynaconf.loaders.redis_loader'
diff --git a/dynaconf/loaders/__init__.py b/dynaconf/loaders/__init__.py
index dbf03d3..e382bc4 100644
--- a/dynaconf/loaders/__init__.py
+++ b/dynaconf/loaders/__init__.py
@@ -1,6 +1,7 @@
 import os
 import importlib
 from dynaconf import default_settings
+from dynaconf.loaders import yaml_loader
 
 
 def default_loader(obj):
@@ -9,9 +10,13 @@ def default_loader(obj):
             obj.set(key, value)
 
 
-def module_loader(obj, settings_module=None):
+def module_loader(obj, settings_module=None, namespace=None):
     settings_module = settings_module or obj.settings_module
-    if not settings_module:
+    if not settings_module:  # pragma: no cover
+        return
+
+    if settings_module.endswith(('.yaml', '.yml')):
+        yaml_loader.load(obj, filename=settings_module, namespace=namespace)
         return
 
     try:
diff --git a/dynaconf/loaders/env_loader.py b/dynaconf/loaders/env_loader.py
index e950ab5..f8c9ef7 100644
--- a/dynaconf/loaders/env_loader.py
+++ b/dynaconf/loaders/env_loader.py
@@ -20,7 +20,7 @@ def load(obj, namespace=None, silent=True, key=None):
                 if key.startswith('%s_' % namespace.upper())
             }
             obj.update(data, loader_identifier=IDENTIFIER)
-    except Exception as e:
+    except Exception as e:  # pragma: no cover
         e.message = 'Unable to load config env namespace (%s)' % e.message
         if silent:
             obj.logger.error(e.message)
diff --git a/dynaconf/loaders/yaml_loader.py b/dynaconf/loaders/yaml_loader.py
new file mode 100644
index 0000000..126b53e
--- /dev/null
+++ b/dynaconf/loaders/yaml_loader.py
@@ -0,0 +1,58 @@
+# coding: utf-8
+try:
+    import yaml
+except ImportError as e:  # pragma: no cover
+    yaml = None
+
+IDENTIFIER = 'yaml_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 yaml 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 yaml is None:  # pragma: no cover
+        raise RuntimeError(
+            "PyYAML package is not installed in your environment.\n"
+            "To use this loader you have to install it with\n"
+            "pip install PyYAML\n"
+            "or\n"
+            "pip install dynaconf[yaml]"
+        )
+
+    filename = filename or obj.get('YAML')
+    if not filename:
+        return
+
+    namespace = namespace or obj.DYNACONF_NAMESPACE
+
+    clean(obj, namespace, identifier=filename)
+
+    if filename.endswith(('.yaml', '.yml')):  # pragma: no cover
+        yaml_data = yaml.load(open(filename))
+    else:
+        # for tests it is possible to pass YAML string
+        yaml_data = yaml.load(filename)
+
+    yaml_data = {key.lower(): value for key, value in yaml_data.items()}
+
+    try:
+        data = yaml_data[namespace.lower()]
+    except KeyError:
+        raise KeyError(
+            '%s namespace not defined in %s' % (namespace, filename)
+        )
+
+    if not key:
+        obj.update(data, loader_identifier=filename)
+    else:
+        obj.set(key, data.get(key))
+
+def clean(obj, namespace, silent=True, identifier=IDENTIFIER):  # noqa
+    for key in obj.loaded_by_loaders.get(identifier, {}):
+        obj.unset(key)
diff --git a/example/yaml_example/settings_module/app.py b/example/yaml_example/settings_module/app.py
new file mode 100644
index 0000000..35d85c8
--- /dev/null
+++ b/example/yaml_example/settings_module/app.py
@@ -0,0 +1,34 @@
+from dynaconf import settings
+
+# export DYNACONF_SETTINGS=settings.yaml
+# or run $ source env.sh
+
+# print all values in dynaconf: namespace of yaml file
+print(settings.HOST)
+print(settings.PORT)
+print(settings.USERNAME)
+print(settings.PASSWORD)
+print(settings.LEVELS)
+print(settings.TEST_LOADERS)
+print(settings.MONEY)
+print(settings.AGE)
+print(settings.ENABLED)
+
+# using development: namespace values for context
+with settings.using_namespace('DEVELOPMENT'):
+    print(settings.ENVIRONMENT)
+    print(settings.HOST)
+
+# back to default dynaconf: namespace
+print(settings.get('ENVIRONMENT'))
+print(settings.HOST)
+
+# set namespace to development:
+settings.namespace('development')
+print(settings.HOST)
+print(settings.ENVIRONMENT)
+
+# back to default namespace again
+settings.namespace()
+print(settings.HOST)
+print(settings.get('ENVIRONMENT'))
\ No newline at end of file
diff --git a/example/yaml_example/settings_module/env.sh b/example/yaml_example/settings_module/env.sh
new file mode 100644
index 0000000..f84de7f
--- /dev/null
+++ b/example/yaml_example/settings_module/env.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+export DYNACONF_SETTINGS=settings.yaml
\ No newline at end of file
diff --git a/example/yaml_example/settings_module/settings.yaml b/example/yaml_example/settings_module/settings.yaml
new file mode 100644
index 0000000..3ed8eb5
--- /dev/null
+++ b/example/yaml_example/settings_module/settings.yaml
@@ -0,0 +1,18 @@
+dynaconf:
+  host: server.com
+  port: 5000
+  username: admin
+  PASSWORD: Secret
+  levels:
+    - debug
+    - info
+    - warning
+  test_loaders:
+    dev: test_dev
+    prod: test_prod
+  money: 500.50
+  age: 42
+  enabled: true
+development:
+  environment: this is development
+  host: dev_server.com
\ No newline at end of file
diff --git a/example/yaml_example/yaml_as_extra_config/app.py b/example/yaml_example/yaml_as_extra_config/app.py
new file mode 100644
index 0000000..37cdf3c
--- /dev/null
+++ b/example/yaml_example/yaml_as_extra_config/app.py
@@ -0,0 +1,15 @@
+from dynaconf import settings
+
+print(settings.YAML)
+print(settings.HOST)
+print(settings.PORT)
+
+
+# using development: namespace values for context
+with settings.using_namespace('DEVELOPMENT'):
+    print(settings.ENVIRONMENT)
+    print(settings.HOST)
+
+# back to default dynaconf: namespace
+print(settings.get('ENVIRONMENT'))
+print(settings.HOST)
\ No newline at end of file
diff --git a/example/yaml_example/yaml_as_extra_config/env.sh b/example/yaml_example/yaml_as_extra_config/env.sh
new file mode 100644
index 0000000..6cf1ad1
--- /dev/null
+++ b/example/yaml_example/yaml_as_extra_config/env.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+# optional if not defined in settings.py
+export DYNACONF_YAML=extra_settings.yaml
diff --git a/example/yaml_example/yaml_as_extra_config/extra_settings.yaml b/example/yaml_example/yaml_as_extra_config/extra_settings.yaml
new file mode 100644
index 0000000..60b5950
--- /dev/null
+++ b/example/yaml_example/yaml_as_extra_config/extra_settings.yaml
@@ -0,0 +1,6 @@
+dynaconf:
+  host: server.com
+  port: 5000
+development:
+  environment: this is development
+  host: dev_server.com
\ No newline at end of file
diff --git a/example/yaml_example/yaml_as_extra_config/settings.py b/example/yaml_example/yaml_as_extra_config/settings.py
new file mode 100644
index 0000000..d38bff3
--- /dev/null
+++ b/example/yaml_example/yaml_as_extra_config/settings.py
@@ -0,0 +1,3 @@
+HOST = 'default.com'
+PORT = 9000
+YAML = 'extra_settings.yaml'
\ No newline at end of file


Test patch:
diff --git a/tests/test_env_loader.py b/tests/test_env_loader.py
new file mode 100644
index 0000000..14e6c1f
--- /dev/null
+++ b/tests/test_env_loader.py
@@ -0,0 +1,23 @@
+import os
+from dynaconf.loaders.env_loader import load
+
+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_DEBUG'] = '@bool true'
+os.environ['PROJECT1_HOSTNAME'] = 'otherhost.com'
+os.environ['PROJECT1_PORT'] = '@int 8080'
+
+from dynaconf import settings  # noqa
+settings.configure()
+
+def test_env_loader():
+    assert settings.HOSTNAME == 'host.com'
+
+
+def test_single_key():
+    load(settings, namespace='PROJECT1', key='HOSTNAME')
+    assert settings.HOSTNAME == 'otherhost.com'
+    assert settings.PORT == 5000
diff --git a/tests/test_yaml_loader.py b/tests/test_yaml_loader.py
new file mode 100644
index 0000000..61e7909
--- /dev/null
+++ b/tests/test_yaml_loader.py
@@ -0,0 +1,57 @@
+import pytest
+from dynaconf import LazySettings
+from dynaconf.loaders.yaml_loader import load
+
+settings = LazySettings(
+    DYNACONF_NAMESPACE='EXAMPLE',
+)
+
+
+YAML = """
+example:
+  host: server.com
+  port: 8080
+development:
+  host: dev_server.com
+"""
+
+def test_load_from_yaml():
+    load(settings, filename=YAML)
+    assert settings.HOST == 'server.com'
+    assert settings.PORT == 8080
+    load(settings, filename=YAML, namespace='DEVELOPMENT')
+    assert settings.HOST == 'dev_server.com'
+    load(settings, filename=YAML)
+    assert settings.HOST == 'server.com'
+
+
+def test_no_filename_is_none():
+    assert load(settings) is None
+
+
+def test_error_on_invalid_namespace():
+    with pytest.raises(KeyError):
+        load(settings, filename=YAML, namespace='FOOBAR')
+
+
+def test_load_single_key():
+    yaml = """
+    foo:
+       bar: blaz
+       zaz: naz
+    """
+    load(settings, filename=yaml, namespace='FOO', key='bar')
+    assert settings.BAR == 'blaz'
+    assert settings.exists('BAR') is True
+    assert settings.exists('ZAZ') is False
+
+
+def test_extra_yaml():
+    load(settings, filename=YAML)
+    yaml = """
+    example:
+       hello: world
+    """
+    settings.set('YAML', yaml)
+    settings.execute_loaders(namespace='EXAMPLE')
+    assert settings.HELLO == 'world'