Repository: huge-success/sanic
Pull request #1436 (2018-12-26 20:37:23)

Config from object string
Implemented support in config_from_object method to receive a path by sting equal similar to Flask.


Patch:
diff --git a/docs/sanic/config.md b/docs/sanic/config.md
index 41405597..3ee2aad8 100644
--- a/docs/sanic/config.md
+++ b/docs/sanic/config.md
@@ -53,6 +53,13 @@ import myapp.default_settings
 app = Sanic('myapp')
 app.config.from_object(myapp.default_settings)
 ```
+or also by path to config:
+
+```
+app = Sanic('myapp')
+app.config.from_object('config.path.config.Class')
+```
+
 
 You could use a class or any other object as well.
 
diff --git a/sanic/config.py b/sanic/config.py
index a7183d77..c785dca5 100644
--- a/sanic/config.py
+++ b/sanic/config.py
@@ -4,6 +4,7 @@ import types
 from distutils.util import strtobool
 
 from sanic.exceptions import PyFileError
+from sanic.helpers import import_string
 
 
 SANIC_PREFIX = "SANIC_"
@@ -101,6 +102,9 @@ class Config(dict):
             from yourapplication import default_config
             app.config.from_object(default_config)
 
+            or also:
+            app.config.from_object('myproject.config.MyConfigClass')
+
         You should not use this function to load the actual configuration but
         rather configuration defaults. The actual config should be loaded
         with :meth:`from_pyfile` and ideally from a location not within the
@@ -108,6 +112,8 @@ class Config(dict):
 
         :param obj: an object holding the configuration
         """
+        if isinstance(obj, str):
+            obj = import_string(obj)
         for key in dir(obj):
             if key.isupper():
                 self[key] = getattr(obj, key)
diff --git a/sanic/helpers.py b/sanic/helpers.py
index 3312f0c7..1b30e1ad 100644
--- a/sanic/helpers.py
+++ b/sanic/helpers.py
@@ -1,5 +1,9 @@
 """Defines basics of HTTP standard."""
 
+from importlib import import_module
+from inspect import ismodule
+
+
 STATUS_CODES = {
     100: b"Continue",
     101: b"Switching Protocols",
@@ -131,3 +135,21 @@ def remove_entity_headers(headers, allowed=("content-location", "expires")):
         if not is_entity_header(header) or header.lower() in allowed
     }
     return headers
+
+
+def import_string(module_name, package=None):
+    """
+    import a module or class by string path.
+
+    :module_name: str with path of module or path to import and
+    instanciate a class
+    :returns: a module object or one instance from class if
+    module_name is a valid path to class
+
+    """
+    module, klass = module_name.rsplit(".", 1)
+    module = import_module(module, package=package)
+    obj = getattr(module, klass)
+    if ismodule(obj):
+        return obj
+    return obj()


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index c2da4bdd..8dcb74f5 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -17,17 +17,30 @@ def temp_path():
         yield Path(td, "file")
 
 
-def test_load_from_object(app):
-    class Config:
-        not_for_config = "should not be used"
-        CONFIG_VALUE = "should be used"
+class ConfigTest:
+        not_for_config = 'should not be used'
+        CONFIG_VALUE = 'should be used'
 
-    app.config.from_object(Config)
+
+def test_load_from_object(app):
+    app.config.from_object(ConfigTest)
     assert "CONFIG_VALUE" in app.config
     assert app.config.CONFIG_VALUE == "should be used"
     assert "not_for_config" not in app.config
 
 
+def test_load_from_object_string(app):
+    app.config.from_object('test_config.ConfigTest')
+    assert 'CONFIG_VALUE' in app.config
+    assert app.config.CONFIG_VALUE == 'should be used'
+    assert 'not_for_config' not in app.config
+
+
+def test_load_from_object_string_exception(app):
+    with pytest.raises(ImportError):
+        app.config.from_object('test_config.Config.test')
+
+
 def test_auto_load_env():
     environ["SANIC_TEST_ANSWER"] = "42"
     app = Sanic()
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 7e7c6ddb..19d0e9e2 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -1,4 +1,8 @@
+import inspect
+
 from sanic import helpers
+from sanic.config import Config
+import pytest
 
 
 def test_has_message_body():
@@ -56,3 +60,18 @@ def test_remove_entity_headers():
 
     for header, expected in tests:
         assert helpers.remove_entity_headers(header) == expected
+
+
+def test_import_string_class():
+    obj = helpers.import_string('sanic.config.Config')
+    assert isinstance(obj, Config)
+
+
+def test_import_string_module():
+    module = helpers.import_string('sanic.config')
+    assert inspect.ismodule(module)
+
+
+def test_import_string_exception():
+    with pytest.raises(ImportError):
+        helpers.import_string('test.test.test')