Tornado utils. Settings
There is options module in tornado similar to what I call settings, it allows to parse options passed via command line or settings file. The options module doesn't cover a few features I use widely: load settings from environ variables and initialize class variables with settings values (options must be available on class initialization, i.e. import). Things I don't like from the options: I usually must execute parse_command_line
or parse_config_file
before using options, there are two options (tornado.options
and tornado.options.options
) that confuses a bit, I want options to be immutable (there must be a special method to change them in runtime). I had tried different approaches to overcome the issues and came up with small patch of tornado options and few conventions.
Usage example
All settings located in the defines.py
file, similar to what we have in options:
# ... Setting(name='MY_SETTING', content_type=int, default=100, description="My setting.")
There are two ways to change the default setting value:
- set environment variable (
export PROJECT_MY_SETTING=101
) - execute app.py with arguments (
app.py --MY_SETTING=101
)
To retrieve setting value just get attribute with the same name:
from project.settings import settings class MyClass: MY_CONST = settings.MY_SETTING
The implementation
Project structure:
- project/ - app.py - project/ - common/ - settings/ - __init__.py - defines.py - models.py - tests.py - __init__.py
project/app.py
:
from project.settings import settings from tornado.ioloop import IOLoop from tornado.web import Application as BaseApplication class Application(BaseApplication): def __init__(self, **kwargs): kwargs['debug'] = settings.DEBUG super(Application, self).__init__(handlers=[], **kwargs) if __name__ == '__main__': application = Application() application.listen(port=settings.PORT) IOLoop.instance().start()
project/project/settings/__init__.py
:
from .models import * from .tests import *
project/project/defines.py:
from collections import namedtuple __all__ = ('DEFINES',) Setting = namedtuple(typename='Setting', field_names=('name', 'content_type', 'default', 'description')) DEFINES = ( Setting(name='DEBUG', content_type=bool, default=True, description="Enable debug mode."), Setting(name='PORT', content_type=int, default=8000, description="Port, default: 8000."), )
project/project/models.py
:
import os from tornado import options from .defines import DEFINES __all__ = ('settings',) class Settings: ENV_PREFIX = 'PROJECT' _setting_parsers = {} def __init__(self, defines): for setting_define in defines: if setting_define.name.upper() != setting_define.name: raise ValueError("Setting name must be uppercase.") if setting_define.content_type not in self._setting_parsers: raise ValueError("Unknown setting content type.") if setting_define.name in self.__dict__: raise ValueError("Invalid setting name.") default = os.getenv( '{prefix}_{attr}'.format( prefix=self.ENV_PREFIX, attr=setting_define.name), setting_define.default ) parser = self._setting_parsers[setting_define.content_type] options.define( setting_define.name, type=parser.OPTIONS_TYPE, default=parser.decode(value=default), help=setting_define.description ) options.parse_command_line() def __setattr__(self, key, value): if key in options.options: raise RuntimeError("Settings are immutable.") super(Settings, self).__setattr__(key=key, value=value) def __getattr__(self, item): return options.options[item] @classmethod def register_content_type(cls, content_type_name, content_type_cls): if content_type_name in cls._setting_parsers: raise ValueError("Duplicate setting content type.") cls._setting_parsers[content_type_name] = content_type_cls def update_setting(self, name, value): setattr(options.options, name, value) class SettingTypeMeta(type): def __init__(cls, name, bases, attrs): super(SettingTypeMeta, cls).__init__(name, bases, attrs) if getattr(cls, 'CONTENT_TYPE_NAMES', None): for content_type_name in cls.CONTENT_TYPE_NAMES: Settings.register_content_type(content_type_name=content_type_name, content_type_cls=cls) class SettingType(metaclass=SettingTypeMeta): CONTENT_TYPE_NAMES = None OPTIONS_TYPE = None class IntegerSettingType(SettingType): CONTENT_TYPE_NAMES = (int, 'int', 'integer') OPTIONS_TYPE = int @classmethod def decode(cls, value): if isinstance(value, int): return value return int(value) class StringSettingType(SettingType): CONTENT_TYPE_NAMES = (str, 'str', 'string') OPTIONS_TYPE = str @classmethod def decode(cls, value): if isinstance(value, str): return value return str(value) class BooleanSettingType(SettingType): CONTENT_TYPE_NAMES = (bool, 'bool', 'boolean') OPTIONS_TYPE = bool @classmethod def decode(cls, value): if isinstance(value, bool): return value if str(value).lower() in ('t', 'true', 'y', 'yes'): return True return False settings = Settings(defines=DEFINES)
project/project/settings/tests.py
:
import os import sys from unittest import TestCase from .defines import Setting from .models import Settings __all__ = ('TornadoSettingsTestCase',) class TornadoSettingsTestCase(TestCase): SETTING_NAME = 'SETTINGS_TEST' ENV_SETTING_NAME = '{prefix}_{name}'.format(prefix=Settings.ENV_PREFIX, name=SETTING_NAME) def setUp(self): self.assertNotIn(self.ENV_SETTING_NAME, os.environ) self._sys_argv = tuple(sys.argv) def test_settings(self): DEFAULT_VALUE = 10 ENVIRON_VALUE = 20 COMMAND_LINE_VALUE = 30 defines = ( Setting(name=self.SETTING_NAME, content_type=int, default=DEFAULT_VALUE, description="Test."), ) settings = Settings(defines=defines) self.assertEqual(getattr(settings, self.SETTING_NAME), DEFAULT_VALUE) os.environ[self.ENV_SETTING_NAME] = str(ENVIRON_VALUE) settings = Settings(defines=defines) self.assertEqual(getattr(settings, self.SETTING_NAME), ENVIRON_VALUE) sys.argv.insert(1, '--{name}={value}'.format(name=self.SETTING_NAME, value=COMMAND_LINE_VALUE)) settings = Settings(defines=defines) self.assertEqual(getattr(settings, self.SETTING_NAME), COMMAND_LINE_VALUE) def test_edit_setting(self): DEFAULT_VALUE = 10 NEW_VALUE = 20 defines = ( Setting(name=self.SETTING_NAME, content_type=int, default=DEFAULT_VALUE, description="Test."), ) settings = Settings(defines=defines) self.assertEqual(getattr(settings, self.SETTING_NAME), DEFAULT_VALUE) self.assertRaises(RuntimeError, setattr, settings, self.SETTING_NAME, NEW_VALUE) self.assertEqual(getattr(settings, self.SETTING_NAME), DEFAULT_VALUE) settings.update_setting(name=self.SETTING_NAME, value=NEW_VALUE) self.assertEqual(getattr(settings, self.SETTING_NAME), NEW_VALUE) def tearDown(self): if self.ENV_SETTING_NAME in os.environ: del os.environ[self.ENV_SETTING_NAME] sys.argv = list(self._sys_argv)