From e978121d58b074c788b19fcbdadd5a5a9ec1fd06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jotag=C3=AA=20Sales?= Date: Wed, 26 Dec 2018 16:23:16 -0200 Subject: [PATCH 1/9] configure app from object by path string --- sanic/config.py | 4 ++++ setup.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sanic/config.py b/sanic/config.py index 0a53a607..d3b03f4f 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,6 +1,8 @@ import os import types +import import_string + from sanic.exceptions import PyFileError @@ -111,6 +113,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/setup.py b/setup.py index 37164336..87de21fb 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ Sanic import codecs import os import re -from distutils.errors import DistutilsPlatformError + from distutils.util import strtobool from setuptools import setup @@ -63,6 +63,7 @@ requirements = [ 'aiofiles>=0.3.0', 'websockets>=6.0,<7.0', 'multidict>=4.0,<5.0', + 'import-string>=0.1.0' ] if strtobool(os.environ.get("SANIC_NO_UJSON", "no")): print("Installing without uJSON") From 0b64fe674670b2b282cb1426b8d738087f6466c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jotag=C3=AA=20Sales?= Date: Wed, 26 Dec 2018 18:27:02 -0200 Subject: [PATCH 2/9] create a documentation for config path --- docs/sanic/config.md | 9 +++++++++ tests/test_config.py | 12 ++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/sanic/config.md b/docs/sanic/config.md index c16e2397..2b934d71 100644 --- a/docs/sanic/config.md +++ b/docs/sanic/config.md @@ -53,6 +53,15 @@ import myapp.default_settings app = Sanic('myapp') app.config.from_object(myapp.default_settings) ``` +or also by path to config: + +``` +import myapp.default_settings + +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/tests/test_config.py b/tests/test_config.py index 8cac5dfc..0f8ef746 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -16,17 +16,25 @@ def temp_path(): yield Path(td, 'file') -def test_load_from_object(app): - class Config: +class Config: not_for_config = 'should not be used' CONFIG_VALUE = 'should be used' + +def test_load_from_object(app): app.config.from_object(Config) 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.Config') + assert 'CONFIG_VALUE' in app.config + assert app.config.CONFIG_VALUE == 'should be used' + assert 'not_for_config' not in app.config + + def test_auto_load_env(): environ["SANIC_TEST_ANSWER"] = "42" app = Sanic() From 19b304b0fc6f200a738b3181e811056465eafb6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jotag=C3=AA=20Sales?= Date: Wed, 26 Dec 2018 18:31:43 -0200 Subject: [PATCH 3/9] fix doc --- docs/sanic/config.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/sanic/config.md b/docs/sanic/config.md index 2b934d71..5fb639f5 100644 --- a/docs/sanic/config.md +++ b/docs/sanic/config.md @@ -56,8 +56,6 @@ app.config.from_object(myapp.default_settings) or also by path to config: ``` -import myapp.default_settings - app = Sanic('myapp') app.config.from_object('config.path.config.Class') ``` From a33ebbaf1104b7a9db69880271a2fd8a149f3b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jotag=C3=AA=20Sales?= Date: Wed, 26 Dec 2018 21:19:54 -0200 Subject: [PATCH 4/9] remove dependence and implmented import_string --- sanic/config.py | 4 +--- sanic/helpers.py | 7 +++++++ setup.py | 1 - tests/test_config.py | 5 +++++ tests/test_helpers.py | 12 ++++++++++++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index d3b03f4f..719528e4 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,10 +1,8 @@ import os import types -import import_string - from sanic.exceptions import PyFileError - +from sanic.helpers import import_string SANIC_PREFIX = "SANIC_" diff --git a/sanic/helpers.py b/sanic/helpers.py index 3312f0c7..d3e54529 100644 --- a/sanic/helpers.py +++ b/sanic/helpers.py @@ -1,4 +1,5 @@ """Defines basics of HTTP standard.""" +from importlib import import_module STATUS_CODES = { 100: b"Continue", @@ -131,3 +132,9 @@ 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): + module, obj = module_name.rsplit('.', 1) + module = import_module(module) + return getattr(module, obj)() diff --git a/setup.py b/setup.py index 87de21fb..38b70da4 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,6 @@ requirements = [ 'aiofiles>=0.3.0', 'websockets>=6.0,<7.0', 'multidict>=4.0,<5.0', - 'import-string>=0.1.0' ] if strtobool(os.environ.get("SANIC_NO_UJSON", "no")): print("Installing without uJSON") diff --git a/tests/test_config.py b/tests/test_config.py index 0f8ef746..885c745e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -35,6 +35,11 @@ def test_load_from_object_string(app): 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 b5f6dd16..db1f9b61 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,4 +1,5 @@ from sanic import helpers +import pytest def test_has_message_body(): @@ -72,3 +73,14 @@ def test_remove_entity_headers(): for header, expected in tests: assert helpers.remove_entity_headers(header) == expected + + +def test_import_string(): + from sanic.config import Config + obj = helpers.import_string('sanic.config.Config') + assert isinstance(obj, Config) + + +def test_import_string_exception(): + with pytest.raises(ImportError): + helpers.import_string('test.test.test') From 375ebd39f0ccc7a00e7514c39308525b7f070f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jotag=C3=AA=20Sales?= Date: Wed, 26 Dec 2018 21:28:42 -0200 Subject: [PATCH 5/9] fix pep8 errors --- tests/test_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index db1f9b61..37a3c5ab 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,4 +1,5 @@ from sanic import helpers +from sanic.config import Config import pytest @@ -76,7 +77,6 @@ def test_remove_entity_headers(): def test_import_string(): - from sanic.config import Config obj = helpers.import_string('sanic.config.Config') assert isinstance(obj, Config) From bf029c1b9de0587eff669ba232b43f27821ee8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jotag=C3=AA=20Sales?= Date: Thu, 27 Dec 2018 14:35:04 -0200 Subject: [PATCH 6/9] added docstring to helper function import_string --- sanic/config.py | 4 ++++ sanic/helpers.py | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index 719528e4..d9eba5ab 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -4,6 +4,7 @@ import types from sanic.exceptions import PyFileError from sanic.helpers import import_string + SANIC_PREFIX = "SANIC_" @@ -104,6 +105,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 diff --git a/sanic/helpers.py b/sanic/helpers.py index d3e54529..75c19152 100644 --- a/sanic/helpers.py +++ b/sanic/helpers.py @@ -1,6 +1,7 @@ """Defines basics of HTTP standard.""" from importlib import import_module + STATUS_CODES = { 100: b"Continue", 101: b"Switching Protocols", @@ -135,6 +136,17 @@ def remove_entity_headers(headers, allowed=("content-location", "expires")): def import_string(module_name): - module, obj = module_name.rsplit('.', 1) + """ + 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) - return getattr(module, obj)() + if hasattr(module, klass): + return getattr(module, klass)() + return module From f2a55d01ea7bca88edadc8292e3fb72a4c6e4d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jotag=C3=AA=20Sales?= Date: Thu, 27 Dec 2018 15:20:58 -0200 Subject: [PATCH 7/9] fix error in import_string --- sanic/helpers.py | 9 ++++++--- tests/test_helpers.py | 9 ++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/sanic/helpers.py b/sanic/helpers.py index 75c19152..2634e8e0 100644 --- a/sanic/helpers.py +++ b/sanic/helpers.py @@ -1,5 +1,7 @@ """Defines basics of HTTP standard.""" + from importlib import import_module +from inspect import ismodule STATUS_CODES = { @@ -147,6 +149,7 @@ def import_string(module_name): """ module, klass = module_name.rsplit(".", 1) module = import_module(module) - if hasattr(module, klass): - return getattr(module, klass)() - return module + obj = getattr(module, klass) + if ismodule(obj): + return obj + return obj() diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 37a3c5ab..7e45a2e8 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,3 +1,5 @@ +import inspect + from sanic import helpers from sanic.config import Config import pytest @@ -76,11 +78,16 @@ def test_remove_entity_headers(): assert helpers.remove_entity_headers(header) == expected -def test_import_string(): +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') From 734730640aaae6b971f6d359b022767b0d075850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jotag=C3=AA=20Sales?= Date: Tue, 5 Mar 2019 01:40:17 -0300 Subject: [PATCH 8/9] added param package to relative imports --- sanic/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/helpers.py b/sanic/helpers.py index 2634e8e0..1b30e1ad 100644 --- a/sanic/helpers.py +++ b/sanic/helpers.py @@ -137,7 +137,7 @@ def remove_entity_headers(headers, allowed=("content-location", "expires")): return headers -def import_string(module_name): +def import_string(module_name, package=None): """ import a module or class by string path. @@ -148,7 +148,7 @@ def import_string(module_name): """ module, klass = module_name.rsplit(".", 1) - module = import_module(module) + module = import_module(module, package=package) obj = getattr(module, klass) if ismodule(obj): return obj From b534df242be4945854a43ddb60ad220d03ffbcbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jotag=C3=AA=20Sales?= Date: Tue, 5 Mar 2019 14:36:54 -0300 Subject: [PATCH 9/9] rename config in class in test_config --- tests/test_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 03db4df3..8dcb74f5 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -17,20 +17,20 @@ def temp_path(): yield Path(td, "file") -class Config: +class ConfigTest: not_for_config = 'should not be used' CONFIG_VALUE = 'should be used' def test_load_from_object(app): - app.config.from_object(Config) + 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.Config') + 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