mirror of
https://github.com/yweber/lodel2.git
synced 2025-10-26 09:39:01 +01:00
[Broken state] started settings implementation
This commit is contained in:
parent
cc578d504d
commit
68a27ff5dd
8 changed files with 384 additions and 171 deletions
72
lodel/plugins.py
Normal file
72
lodel/plugins.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
import os.path
|
||||
|
||||
from importlib.machinery import SourceFileLoader, SourcelessFileLoader
|
||||
|
||||
## @package lodel.plugins Lodel2 plugins management
|
||||
#
|
||||
# Lodel2 plugins are stored in directories
|
||||
# A typicall lodel2 plugin directory structure looks like :
|
||||
# - {{__init__.py}}} containing informations like full_name, authors, licence etc.
|
||||
# - main.py containing hooks registration etc
|
||||
# - confspec.py containing a configuration specification dictionary named CONFSPEC
|
||||
|
||||
VIRTUAL_PACKAGE_NAME = 'lodel.plugins_pkg'
|
||||
CONFSPEC_NAME = 'confspec.py'
|
||||
|
||||
class Plugins(object):
|
||||
|
||||
## @brief Stores plugin directories paths
|
||||
_plugin_directories = None
|
||||
## @brief Optimisation cache storage for plugin paths
|
||||
_plugin_paths = dict()
|
||||
|
||||
def __init__(self):
|
||||
self.started()
|
||||
|
||||
## @brief Given a plugin name returns the plugin path
|
||||
# @param plugin_name str : The plugin name
|
||||
# @return the plugin directory path
|
||||
@classmethod
|
||||
def plugin_path(cls, plugin_name):
|
||||
cls.started()
|
||||
try:
|
||||
return cls._plugin_paths[plugin_name]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
path = None
|
||||
for cur_path in cls._plugin_directories:
|
||||
plugin_path = os.path.join(cur_path, plugin_name)+'/'
|
||||
print(plugin_path)
|
||||
if os.path.isdir(plugin_path):
|
||||
return plugin_path
|
||||
raise NameError("No plugin named '%s'" % plugin_name)
|
||||
|
||||
## @brief Fetch a confspec given a plugin_name
|
||||
# @param plugin_name str : The plugin name
|
||||
# @return a dict of conf spec
|
||||
@classmethod
|
||||
def get_confspec(cls, plugin_name):
|
||||
cls.started()
|
||||
plugin_path = cls.plugin_path(plugin_name)
|
||||
plugin_module = '%s.%s' % ( VIRTUAL_PACKAGE_NAME,
|
||||
plugin_name)
|
||||
conf_spec_module = plugin_module + '.confspec'
|
||||
|
||||
conf_spec_source = plugin_path + CONFSPEC_NAME
|
||||
|
||||
loader = SourceFileLoader(conf_spec_module, conf_spec_source)
|
||||
confspec_module = loader.load_module()
|
||||
return getattr(confspec_module, 'CONFSPEC')
|
||||
|
||||
@classmethod
|
||||
def bootstrap(cls, plugins_directories):
|
||||
cls._plugin_directories = plugins_directories
|
||||
|
||||
@classmethod
|
||||
def started(cls, raise_if_not = True):
|
||||
res = cls._plugin_directories is not None
|
||||
if raise_if_not and not res:
|
||||
raise RuntimeError("Class Plugins is not initialized")
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
import types
|
||||
import warnings
|
||||
from . import settings_format
|
||||
|
||||
## @package Lodel.settings
|
||||
#
|
||||
# @brief Defines stuff to handles Lodel2 configuration (see @ref lodel_settings )
|
||||
#
|
||||
# To access the confs use the Lodel.settings.Settings SettingsHandler instance
|
||||
|
||||
## @brief A class designed to handles Lodel2 settings
|
||||
#
|
||||
# When instanciating a SettingsHandler, the new instance is filled with the content of settings.py (in the root directory of lodel2
|
||||
#
|
||||
# @warning You don't have to instanciate this class, you can access to the global instance with the Settings variable in this module
|
||||
# @todo broken stuff... Rewrite it
|
||||
# @todo Forbid module assignement in settings ! and disable tests about this
|
||||
# @todo Implements a type checking of config value
|
||||
# @todo Implements default values for config keys
|
||||
class SettingsHandler(object):
|
||||
|
||||
## @brief Shortcut
|
||||
_allowed = settings_format.ALLOWED + settings_format.MANDATORY
|
||||
## @brief Shortcut
|
||||
_mandatory = settings_format.MANDATORY
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
import settings as default_settings
|
||||
self._load_module(default_settings)
|
||||
except ImportError:
|
||||
warnings.warn("Unable to find global default settings")
|
||||
|
||||
## @brief A flag set to True when the instance is fully loaded
|
||||
self._set_loaded(False if len(self._missings()) > 0 else True)
|
||||
|
||||
## @brief Compat wrapper for getattr
|
||||
def get(self, name):
|
||||
return getattr(self, name)
|
||||
|
||||
## @brief Compat wrapper for setattr
|
||||
def set(self, name, value):
|
||||
return setattr(self, name, value)
|
||||
|
||||
## @brief Load every module properties in the settings instance
|
||||
#
|
||||
# Load a module content into a SettingsHandler instance and checks that no mandatory settings are missing
|
||||
# @note Example : <pre> import my_cool_settings;
|
||||
# Settings._load_module(my_cool_settings);</pre>
|
||||
# @param module module|None: a loaded module (if None just check for missing settings)
|
||||
# @throw LookupError if invalid settings found or if mandatory settings are missing
|
||||
def load_module(self, module = None):
|
||||
if not(module is None):
|
||||
self._load_module(module)
|
||||
missings = self._missings()
|
||||
if len(missings) > 0:
|
||||
self._loaded = False
|
||||
raise LookupError("Mandatory settings are missing : %s"%missings)
|
||||
self._set_loaded(True)
|
||||
|
||||
## @brief supersede of default __setattr__ method
|
||||
def __setattr__(self, name, value):
|
||||
if not hasattr(self, name):
|
||||
if name not in self._allowed:
|
||||
raise LookupError("Invalid setting : %s"%name)
|
||||
super().__setattr__(name, value)
|
||||
|
||||
## @brief This method do the job for SettingsHandler.load_module()
|
||||
#
|
||||
# @note The difference with SettingsHandler.load_module() is that it didn't check if some settings are missing
|
||||
# @throw LokkupError if an invalid settings is given
|
||||
# @param module : a loaded module
|
||||
def _load_module(self, module):
|
||||
errors = []
|
||||
fatal_errors = []
|
||||
conf_dict = {
|
||||
name: getattr(module, name)
|
||||
for name in dir(module)
|
||||
if not name.startswith('__') and not isinstance(getattr(module, name), types.ModuleType)
|
||||
}
|
||||
for name, value in conf_dict.items():
|
||||
try:
|
||||
setattr(self, name, value)
|
||||
except LookupError:
|
||||
errors.append(name)
|
||||
if len(errors) > 0:
|
||||
err_msg = "Found invalid settings in %s : %s"%(module.__name__, errors)
|
||||
raise LookupError(err_msg)
|
||||
|
||||
## @brief Refresh the allowed and mandatory settings list
|
||||
@classmethod
|
||||
def _refresh_format(cls):
|
||||
## @brief Shortcut
|
||||
cls._allowed = settings_format.ALLOWED + settings_format.MANDATORY
|
||||
## @brief Shortcut
|
||||
cls._mandatory = settings_format.MANDATORY
|
||||
|
||||
## @brief If some settings are missings return their names
|
||||
# @return an array of string
|
||||
def _missings(self):
|
||||
return [ confname for confname in self._mandatory if not hasattr(self, confname) ]
|
||||
|
||||
def _set_loaded(self, value):
|
||||
super().__setattr__('_loaded', bool(value))
|
||||
|
||||
Settings = SettingsHandler()
|
||||
|
||||
## @page lodel_settings Lodel SettingsHandler
|
||||
#
|
||||
# This page describe the way settings are handled in Lodel2.
|
||||
#
|
||||
# @section lodel_settings_files Lodel settings files
|
||||
#
|
||||
# - Lodel/settings.py defines the Lodel.settings package, the SettingsHandler class and the Lodel.settings.Settings instance
|
||||
# - Lodel/settings_format.py defines the mandatory and allowed configurations keys lists
|
||||
# - install/instance_settings.py is a model of the file that will be deployed in Lodel2 instances directories
|
||||
#
|
||||
# @section Using Lodel.settings.Settings SettingsHandler instance
|
||||
#
|
||||
# @subsection lodel_settings_without_loader Without loader
|
||||
#
|
||||
# Without any loader you can import Lodel.settings.Settings and acces its property with getattr (or . ) or with SettingsHandler.get() method.
|
||||
# In the same way you can set a settings by standart affectation of a propery or with SettingsHandler.set() method.
|
||||
#
|
||||
# @subsection lodel_settings_loader With a loader in a lodel2 instance
|
||||
#
|
||||
# The loader will import Lodel.settings.Settings and then calls the SettingsHandler.load_module() method to load the content of the instance_settings.py file into the SettingsHandler instance
|
||||
#
|
||||
# @subsection lodel_settings_example Examples
|
||||
#
|
||||
# <pre>
|
||||
# #!/usr/bin/python
|
||||
# from Lodel.settings import Settings
|
||||
# if Settings.debug:
|
||||
# print("DEBUG")
|
||||
# # or
|
||||
# if Settings.get('debug'):
|
||||
# print("DEBUG")
|
||||
# Settings.debug = False
|
||||
# # or
|
||||
# Settings.set('debug', False)
|
||||
# </pre>
|
||||
#
|
||||
102
lodel/settings/settings.py
Normal file
102
lodel/settings/settings.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import os
|
||||
import configparser
|
||||
|
||||
from lodel.plugins import Plugins
|
||||
from lodel.settings.utils import SettingsError, SettingsErrors
|
||||
from lodel.settings.validator import SettingValidator
|
||||
from lodel.settings.settings_loader import SettingsLoader
|
||||
|
||||
## @package lodel.settings Lodel2 settings package
|
||||
#
|
||||
# Contains all module that help handling settings
|
||||
|
||||
## @package lodel.settings.settings Lodel2 settings module
|
||||
#
|
||||
# Handles configuration load/parse/check.
|
||||
#
|
||||
# @subsection Configuration load process
|
||||
#
|
||||
# The configuration load process is not trivial. In fact loaded plugins are able to add their own options.
|
||||
# But the list of plugins to load and the plugins options are in the same file, the instance configuration file.
|
||||
#
|
||||
# @subsection Configuration specification
|
||||
#
|
||||
# Configuration specification is divided in 2 parts :
|
||||
# - default values
|
||||
# - value validation/cast (see @ref Lodel.settings.validator.ConfValidator )
|
||||
#
|
||||
|
||||
PYTHON_SYS_LIB_PATH = '/usr/local/lib/python{major}.{minor}/'.format(
|
||||
|
||||
major = sys.version_info.major,
|
||||
minor = sys.version_info.minor)
|
||||
## @brief Handles configuration load etc.
|
||||
class Settings(object):
|
||||
|
||||
## @brief global conf specsification (default_value + validator)
|
||||
_conf_preload = {
|
||||
'lib_path': ( PYTHON_SYS_LIB_PATH+'/lodel2/',
|
||||
SettingValidator('directory')),
|
||||
'plugins_path': ( PYTHON_SYS_LIB_PATH+'lodel2/plugins/',
|
||||
SettingValidator('directory_list')),
|
||||
}
|
||||
|
||||
def __init__(self, conf_file = '/etc/lodel2/lodel2.conf', conf_dir = 'conf.d'):
|
||||
self.__confs = dict()
|
||||
|
||||
self.__load_bootstrap_conf(conf_file)
|
||||
# now we should have the self.__confs['lodel2']['plugins_paths'] and
|
||||
# self.__confs['lodel2']['lib_path'] set
|
||||
self.__bootstrap()
|
||||
|
||||
## @brief This method handlers Settings instance bootstraping
|
||||
def __bootstrap(self):
|
||||
#loader = SettingsLoader(self.__conf_dir)
|
||||
|
||||
# Starting the Plugins class
|
||||
Plugins.bootstrap(self.__confs['lodel2']['plugins_path'])
|
||||
specs = Plugins.get_confspec('dummy')
|
||||
print("Got specs : %s " % specs)
|
||||
|
||||
# then fetch options values from conf specs
|
||||
|
||||
## @brief Load base global configurations keys
|
||||
#
|
||||
# Base configurations keys are :
|
||||
# - lodel2 lib path
|
||||
# - lodel2 plugins path
|
||||
#
|
||||
# @note return nothing but set the __confs attribute
|
||||
# @see Settings._conf_preload
|
||||
def __load_bootstrap_conf(self, conf_file):
|
||||
config = configparser.ConfigParser()
|
||||
config.read(conf_file)
|
||||
sections = config.sections()
|
||||
if len(sections) != 1 or sections[0].lower() != 'lodel2':
|
||||
raise SettingsError("Global conf error, expected lodel2 section not found")
|
||||
|
||||
#Load default values in result
|
||||
res = dict()
|
||||
for keyname, (keyvalue, validator) in self._conf_preload.items():
|
||||
res[keyname] = keyvalue
|
||||
|
||||
confs = config[sections[0]]
|
||||
errors = []
|
||||
for name in confs:
|
||||
if name not in res:
|
||||
errors.append( SettingsError(
|
||||
"Unknow field",
|
||||
"lodel2.%s" % name,
|
||||
conf_file))
|
||||
try:
|
||||
res[name] = self._conf_preload[name][1](confs[name])
|
||||
except Exception as e:
|
||||
errors.append(SettingsError(str(e), name, conf_file))
|
||||
if len(errors) > 0:
|
||||
raise SettingsErrors(errors)
|
||||
|
||||
self.__confs['lodel2'] = res
|
||||
|
||||
|
|
@ -23,3 +23,22 @@ class SettingsError(Exception):
|
|||
res += ": %s" % (self.__msg)
|
||||
return res
|
||||
|
||||
## @brief Designed to handles mutliple SettingsError
|
||||
class SettingsErrors(Exception):
|
||||
|
||||
## @brief Instanciate an SettingsErrors
|
||||
# @param exceptions list : list of SettingsError instance
|
||||
def __init__(self, exceptions):
|
||||
for expt in exceptions:
|
||||
if not isinstance(expt, SettingsError):
|
||||
raise ValueError("The 'exceptions' argument has to be an array of <class SettingsError>, but a %s was found in the list" % type(expt))
|
||||
self.__exceptions = exceptions
|
||||
|
||||
|
||||
def __repr__(self): return str(self)
|
||||
|
||||
def __str__(self):
|
||||
res = "Errors :\n"
|
||||
for expt in self.__exceptions:
|
||||
res += "\t%s\n" % str(expt)
|
||||
return res
|
||||
|
|
|
|||
181
lodel/settings/validator.py
Normal file
181
lodel/settings/validator.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import os.path
|
||||
import re
|
||||
import inspect
|
||||
import copy
|
||||
|
||||
## @package lodel.settings.validator Lodel2 settings validators/cast module
|
||||
#
|
||||
# Validator are registered in the SettingValidator class.
|
||||
|
||||
class SettingsValidationError(Exception):
|
||||
pass
|
||||
|
||||
## @brief Handles settings validators
|
||||
#
|
||||
# Class instance are callable objects that takes a value argument (the value to validate). It raises
|
||||
# a SettingsValidationError if validation fails, else it returns a properly
|
||||
# casted value.
|
||||
class SettingValidator(object):
|
||||
|
||||
_validators = dict()
|
||||
_description = dict()
|
||||
|
||||
## @brief Instanciate a validator
|
||||
def __init__(self, name, none_is_valid = False):
|
||||
if name is not None and name not in self._validators:
|
||||
raise NameError("No validator named '%s'" % name)
|
||||
self.__name = name
|
||||
|
||||
## @brief Call the validator
|
||||
# @param value *
|
||||
# @return properly casted value
|
||||
# @throw SettingsValidationError
|
||||
def __call__(self, value):
|
||||
if self.__name is None:
|
||||
return value
|
||||
try:
|
||||
return self._validators[self.__name](value)
|
||||
except Exception as e:
|
||||
raise SettingsValidationError(e)
|
||||
|
||||
## @brief Register a new validator
|
||||
# @param name str : validator name
|
||||
# @param callback callable : the function that will validate a value
|
||||
@classmethod
|
||||
def register_validator(cls, name, callback, description=None):
|
||||
if name in cls._validators:
|
||||
raise NameError("A validator named '%s' allready exists" % name)
|
||||
# Broken test for callable
|
||||
if not inspect.isfunction(callback) and not inspect.ismethod(callback) and not hasattr(callback, '__call__'):
|
||||
raise TypeError("Callable expected but got %s" % type(callback))
|
||||
cls._validators[name] = callback
|
||||
cls._description[name] = description
|
||||
|
||||
## @brief Get the validator list associated with description
|
||||
@classmethod
|
||||
def validators_list(cls):
|
||||
return copy.copy(cls._description)
|
||||
|
||||
## @brief Create and register a list validator
|
||||
# @param elt_validator callable : The validator that will be used for validate each elt value
|
||||
# @param validator_name str
|
||||
# @param description None | str
|
||||
# @param separator str : The element separator
|
||||
# @return A SettingValidator instance
|
||||
@classmethod
|
||||
def create_list_validator(cls, validator_name, elt_validator, description = None, separator = ','):
|
||||
def list_validator(value):
|
||||
res = list()
|
||||
errors = list()
|
||||
for elt in value.split(separator):
|
||||
res.append(elt_validator(elt))
|
||||
return res
|
||||
description = "Convert value to an array" if description is None else description
|
||||
cls.register_validator(
|
||||
validator_name,
|
||||
list_validator,
|
||||
description)
|
||||
return cls(validator_name)
|
||||
|
||||
## @brief Create and register a regular expression validator
|
||||
# @param pattern str : regex pattern
|
||||
# @param validator_name str : The validator name
|
||||
# @param description str : Validator description
|
||||
# @return a SettingValidator instance
|
||||
@classmethod
|
||||
def create_re_validator(cls, pattern, validator_name, description = None):
|
||||
def re_validator(value):
|
||||
if not re.match(pattern, value):
|
||||
raise SettingsValidationError("The value '%s' doesn't match the following pattern '%s'" % pattern)
|
||||
return value
|
||||
#registering the validator
|
||||
cls.register_validator(
|
||||
validator_name,
|
||||
re_validator,
|
||||
("Match value to '%s'" % pattern) if description is None else description)
|
||||
return cls(validator_name)
|
||||
|
||||
|
||||
## @return a list of registered validators
|
||||
def validators_list_str(cls):
|
||||
result = ''
|
||||
for name in cls._validators:
|
||||
result += "\t%s" % name
|
||||
if name in self._description and self._description[name] is not None:
|
||||
result += "\t: %s" % self._description[name]
|
||||
result += "\n"
|
||||
return result
|
||||
|
||||
## @brief Integer value validator callback
|
||||
def int_val(value):
|
||||
return int(value)
|
||||
|
||||
## @brief Output file validator callback
|
||||
# @return A file object (if filename is '-' return sys.stderr)
|
||||
def file_err_output(value):
|
||||
if not isinstance(value, str):
|
||||
raise SettingsValidationError("A string was expected but got '%s' " % value)
|
||||
if value == '-':
|
||||
return sys.stderr
|
||||
return value
|
||||
|
||||
## @brief Boolean value validator callback
|
||||
def boolean_val(value):
|
||||
if not (value is True) and not (value is False):
|
||||
raise SettingsValidationError("A boolean was expected but got '%s' " % value)
|
||||
return bool(value)
|
||||
|
||||
def directory_val(value):
|
||||
res = SettingValidator('strip')(value)
|
||||
if not os.path.isdir(res):
|
||||
raise SettingsValidationError("Folowing path don't exists or is not a directory : '%s'"%res)
|
||||
return res
|
||||
|
||||
#
|
||||
# Default validators registration
|
||||
#
|
||||
|
||||
SettingValidator.register_validator(
|
||||
'strip',
|
||||
str.strip,
|
||||
'String trim')
|
||||
|
||||
SettingValidator.register_validator(
|
||||
'int',
|
||||
int_val,
|
||||
'Integer value validator')
|
||||
|
||||
SettingValidator.register_validator(
|
||||
'bool',
|
||||
boolean_val,
|
||||
'Boolean value validator')
|
||||
|
||||
SettingValidator.register_validator(
|
||||
'errfile',
|
||||
file_err_output,
|
||||
'Error output file validator (return stderr if filename is "-")')
|
||||
|
||||
SettingValidator.register_validator(
|
||||
'directory',
|
||||
directory_val,
|
||||
'Directory path validator')
|
||||
|
||||
SettingValidator.create_list_validator(
|
||||
'list',
|
||||
SettingValidator('strip'),
|
||||
description = "Simple list validator. Validate a list of values separated by ','",
|
||||
separator = ',')
|
||||
|
||||
SettingValidator.create_list_validator(
|
||||
'directory_list',
|
||||
SettingValidator('directory'),
|
||||
description = "Validator for a list of directory path separated with ','",
|
||||
separator = ',')
|
||||
|
||||
SettingValidator.create_re_validator(
|
||||
r'^https?://[^\./]+.[^\./]+/?.*$',
|
||||
'http_url',
|
||||
'Url validator')
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
## @package Lodel.settings_format Rules for settings
|
||||
|
||||
## @brief List mandatory configurations keys
|
||||
MANDATORY = [
|
||||
'debug',
|
||||
'debug_sql',
|
||||
'sitename',
|
||||
'lodel2_lib_path',
|
||||
'em_file',
|
||||
'dynamic_code_file',
|
||||
'ds_package',
|
||||
'datasource',
|
||||
'mh_classname',
|
||||
'migration_options',
|
||||
'base_path',
|
||||
'plugins',
|
||||
'logging',
|
||||
]
|
||||
|
||||
## @brief List allowed (but not mandatory) configurations keys
|
||||
ALLOWED = [
|
||||
'em_graph_output',
|
||||
'em_graph_format',
|
||||
'templates_base_dir'
|
||||
]
|
||||
7
plugins/dummy/confspec.py
Normal file
7
plugins/dummy/confspec.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
CONFSPEC = {
|
||||
'section1': {
|
||||
'key1': None
|
||||
}
|
||||
}
|
||||
3
settings.ini
Normal file
3
settings.ini
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[lodel2]
|
||||
lib_path = /home/yannweb/dev/lodel2/lodel2-git
|
||||
plugins_path = /home/yannweb/dev/lodel2/lodel2-git/plugins
|
||||
Loading…
Add table
Add a link
Reference in a new issue