Merge pull request #199 from Tim-Erwin/improved_config
added methods to load config from a file
This commit is contained in:
commit
f56c5e3a45
78
docs/config.md
Normal file
78
docs/config.md
Normal file
|
@ -0,0 +1,78 @@
|
|||
# Configuration
|
||||
|
||||
Any reasonably complex application will need configuration that is not baked into the acutal code. Settings might be different for different environments or installations.
|
||||
|
||||
## Basics
|
||||
|
||||
Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary:
|
||||
|
||||
```
|
||||
app = Sanic('myapp')
|
||||
app.config.DB_NAME = 'appdb'
|
||||
app.config.DB_USER = 'appuser'
|
||||
```
|
||||
|
||||
Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once:
|
||||
|
||||
```
|
||||
db_settings = {
|
||||
'DB_HOST': 'localhost',
|
||||
'DB_NAME': 'appdb',
|
||||
'DB_USER': 'appuser'
|
||||
}
|
||||
app.config.update(db_settings)
|
||||
```
|
||||
|
||||
In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters.
|
||||
|
||||
## Loading Configuration
|
||||
|
||||
There are several ways how to load configuration.
|
||||
|
||||
### From an Object
|
||||
|
||||
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
|
||||
|
||||
```
|
||||
import myapp.default_settings
|
||||
|
||||
app = Sanic('myapp')
|
||||
app.config.from_object(myapp.default_settings)
|
||||
```
|
||||
|
||||
You could use a class or any other object as well.
|
||||
|
||||
### From a File
|
||||
|
||||
Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_file(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:
|
||||
|
||||
```
|
||||
app = Sanic('myapp')
|
||||
app.config.from_envvar('MYAPP_SETTINGS')
|
||||
```
|
||||
|
||||
Then you can run your application with the `MYAPP_SETTINGS` environment variable set:
|
||||
|
||||
```
|
||||
$ MYAPP_SETTINGS=/path/to/config_file; python3 myapp.py
|
||||
INFO: Goin' Fast @ http://0.0.0.0:8000
|
||||
```
|
||||
|
||||
The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase varibales are added to the configuration. Most commonly the configuration consists of simple key value pairs:
|
||||
|
||||
```
|
||||
# config_file
|
||||
DB_HOST = 'localhost'
|
||||
DB_NAME = 'appdb'
|
||||
DB_USER = 'appuser'
|
||||
```
|
||||
|
||||
## Builtin Configuration Values
|
||||
|
||||
Out of the box there are just a few predefined values which can be overwritten when creating the application.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ----------------- | --------- | --------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
|
||||
|
|
@ -15,6 +15,7 @@ Guides
|
|||
exceptions
|
||||
blueprints
|
||||
class_based_views
|
||||
config
|
||||
cookies
|
||||
custom_protocol
|
||||
testing
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
class Config:
|
||||
LOGO = """
|
||||
import os
|
||||
import types
|
||||
|
||||
|
||||
class Config(dict):
|
||||
def __init__(self, defaults=None):
|
||||
super().__init__(defaults or {})
|
||||
self.LOGO = """
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \\
|
||||
|
@ -20,6 +26,65 @@ class Config:
|
|||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
||||
"""
|
||||
REQUEST_MAX_SIZE = 100000000 # 100 megababies
|
||||
REQUEST_TIMEOUT = 60 # 60 seconds
|
||||
ROUTER_CACHE_SIZE = 1024
|
||||
self.REQUEST_MAX_SIZE = 100000000 # 100 megababies
|
||||
self.REQUEST_TIMEOUT = 60 # 60 seconds
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
return self[attr]
|
||||
except KeyError as ke:
|
||||
raise AttributeError("Config has no '{}'".format(ke.args[0]))
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
self[attr] = value
|
||||
|
||||
def from_envvar(self, variable_name):
|
||||
"""Loads a configuration from an environment variable pointing to
|
||||
a configuration file.
|
||||
:param variable_name: name of the environment variable
|
||||
:return: bool. ``True`` if able to load config, ``False`` otherwise.
|
||||
"""
|
||||
config_file = os.environ.get(variable_name)
|
||||
if not config_file:
|
||||
raise RuntimeError('The environment variable %r is not set and '
|
||||
'thus configuration could not be loaded.' %
|
||||
variable_name)
|
||||
return self.from_pyfile(config_file)
|
||||
|
||||
def from_pyfile(self, filename):
|
||||
"""Updates the values in the config from a Python file. Only the uppercase
|
||||
variables in that module are stored in the config.
|
||||
:param filename: an absolute path to the config file
|
||||
"""
|
||||
module = types.ModuleType('config')
|
||||
module.__file__ = filename
|
||||
try:
|
||||
with open(filename) as config_file:
|
||||
exec(compile(config_file.read(), filename, 'exec'),
|
||||
module.__dict__)
|
||||
except IOError as e:
|
||||
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
|
||||
raise
|
||||
self.from_object(module)
|
||||
return True
|
||||
|
||||
def from_object(self, obj):
|
||||
"""Updates the values from the given object.
|
||||
Objects are usually either modules or classes.
|
||||
|
||||
Just the uppercase variables in that object are stored in the config.
|
||||
Example usage::
|
||||
|
||||
from yourapplication import default_config
|
||||
app.config.from_object(default_config)
|
||||
|
||||
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
|
||||
"""
|
||||
for key in dir(obj):
|
||||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import re
|
||||
from collections import defaultdict, namedtuple
|
||||
from functools import lru_cache
|
||||
from .config import Config
|
||||
from .exceptions import NotFound, InvalidUsage
|
||||
from .views import CompositionView
|
||||
|
||||
|
@ -15,6 +14,8 @@ REGEX_TYPES = {
|
|||
'alpha': (str, r'[A-Za-z]+'),
|
||||
}
|
||||
|
||||
ROUTER_CACHE_SIZE = 1024
|
||||
|
||||
|
||||
def url_hash(url):
|
||||
return url.count('/')
|
||||
|
@ -198,7 +199,7 @@ class Router:
|
|||
return self._get(request.url, request.method,
|
||||
request.headers.get("Host", ''))
|
||||
|
||||
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def _get(self, url, method, host):
|
||||
"""
|
||||
Gets a request handler based on the URL of the request, or raises an
|
||||
|
|
76
tests/test_config.py
Normal file
76
tests/test_config.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
from os import environ
|
||||
import pytest
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
def test_load_from_object():
|
||||
app = Sanic('test_load_from_object')
|
||||
class Config:
|
||||
not_for_config = 'should not be used'
|
||||
CONFIG_VALUE = 'should be used'
|
||||
|
||||
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_file():
|
||||
app = Sanic('test_load_from_file')
|
||||
config = b"""
|
||||
VALUE = 'some value'
|
||||
condition = 1 == 1
|
||||
if condition:
|
||||
CONDITIONAL = 'should be set'
|
||||
"""
|
||||
with NamedTemporaryFile() as config_file:
|
||||
config_file.write(config)
|
||||
config_file.seek(0)
|
||||
app.config.from_pyfile(config_file.name)
|
||||
assert 'VALUE' in app.config
|
||||
assert app.config.VALUE == 'some value'
|
||||
assert 'CONDITIONAL' in app.config
|
||||
assert app.config.CONDITIONAL == 'should be set'
|
||||
assert 'condition' not in app.config
|
||||
|
||||
|
||||
def test_load_from_missing_file():
|
||||
app = Sanic('test_load_from_missing_file')
|
||||
with pytest.raises(IOError):
|
||||
app.config.from_pyfile('non-existent file')
|
||||
|
||||
|
||||
def test_load_from_envvar():
|
||||
app = Sanic('test_load_from_envvar')
|
||||
config = b"VALUE = 'some value'"
|
||||
with NamedTemporaryFile() as config_file:
|
||||
config_file.write(config)
|
||||
config_file.seek(0)
|
||||
environ['APP_CONFIG'] = config_file.name
|
||||
app.config.from_envvar('APP_CONFIG')
|
||||
assert 'VALUE' in app.config
|
||||
assert app.config.VALUE == 'some value'
|
||||
|
||||
|
||||
def test_load_from_missing_envvar():
|
||||
app = Sanic('test_load_from_missing_envvar')
|
||||
with pytest.raises(RuntimeError):
|
||||
app.config.from_envvar('non-existent variable')
|
||||
|
||||
|
||||
def test_overwrite_exisiting_config():
|
||||
app = Sanic('test_overwrite_exisiting_config')
|
||||
app.config.DEFAULT = 1
|
||||
class Config:
|
||||
DEFAULT = 2
|
||||
|
||||
app.config.from_object(Config)
|
||||
assert app.config.DEFAULT == 2
|
||||
|
||||
|
||||
def test_missing_config():
|
||||
app = Sanic('test_missing_config')
|
||||
with pytest.raises(AttributeError):
|
||||
app.config.NON_EXISTENT
|
Loading…
Reference in New Issue
Block a user