diff --git a/docs/sanic/config.md b/docs/sanic/config.md index 4140559708..3ee2aad8dd 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 a7183d77a7..c785dca550 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -4,6 +4,7 @@ from distutils.util import strtobool from sanic.exceptions import PyFileError +from sanic.helpers import import_string SANIC_PREFIX = "SANIC_" @@ -101,6 +102,9 @@ def from_object(self, obj): 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 @@ def from_object(self, obj): :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 3312f0c702..1b30e1ad7e 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() diff --git a/tests/test_config.py b/tests/test_config.py index c2da4bdd3f..8dcb74f5c6 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 7e7c6ddbab..19d0e9e251 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')