Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config from object string #1436

Merged
merged 13 commits into from
Jun 16, 2019
Merged
7 changes: 7 additions & 0 deletions docs/sanic/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ import myapp.default_settings
app = Sanic('myapp')
app.config.from_object(myapp.default_settings)
```
or also by path to config:

```
sjsadowski marked this conversation as resolved.
Show resolved Hide resolved
app = Sanic('myapp')
app.config.from_object('config.path.config.Class')
jotagesales marked this conversation as resolved.
Show resolved Hide resolved
```


You could use a class or any other object as well.

Expand Down
6 changes: 6 additions & 0 deletions sanic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import types

from sanic.exceptions import PyFileError
from sanic.helpers import import_string


SANIC_PREFIX = "SANIC_"
Expand Down Expand Up @@ -104,13 +105,18 @@ 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
package because the package might be installed system wide.

: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)
Expand Down
22 changes: 22 additions & 0 deletions sanic/helpers.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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):
"""
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be overthinking this, but would it be a good idea to support relative string paths?

Copy link
Contributor Author

@jotagesales jotagesales Dec 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, i use this function in Flask for configure app using config_from_object and is very good.
Because is common in application has config classes.
example:

class Config:
      DEBUG = False

class Develop(Config):
    DEBUG = True

class Production(Config):
      pass

In this case the user instanciate Sanic class and configure according from environment pass the string path of the class get from environment variable for example.

Copy link
Contributor

@harshanarayana harshanarayana Dec 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case the user instanciate Sanic class and configure according from environment pass the string path of the class get from environment variable for example.

Oh I am totally with you on this feature. I was just curious if it might be possible to support relative path as well.

i.e. Instead of module1.module2.Class, if you are already inside module1, then module2.Class should suffice.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@harshanarayana I think that the importlib searches the entire loaded packages, so I guess this already works out of the box - I might be wrong, testing still needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vltr It works, but the method invocation might need to be tweaked a bit to provide another helper argument. import_module(name, package=None)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@harshanarayana oh, ok, got it. Some tweaking may be needed then ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jotagesales looks like this is still outstanding - can you make changes so we can get this in soon?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sjsadowski ready ;)

module = import_module(module)
obj = getattr(module, klass)
if ismodule(obj):
return obj
return obj()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import codecs
import os
import re
from distutils.errors import DistutilsPlatformError

from distutils.util import strtobool

from setuptools import setup
Expand Down
17 changes: 15 additions & 2 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,30 @@ def temp_path():
yield Path(td, 'file')


def test_load_from_object(app):
class Config:
class Config:
Copy link
Contributor

@harshanarayana harshanarayana Mar 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please rename this class from Config to something else? This line is overriding the import done by from sanic.config import Config

This is causing tests to fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@harshanarayana i renamed the class, thank.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming this class seems to be the only thing holding it up.

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_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()
Expand Down
19 changes: 19 additions & 0 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import inspect

from sanic import helpers
from sanic.config import Config
import pytest


def test_has_message_body():
Expand Down Expand Up @@ -72,3 +76,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')