From 2739a94e5adb1733e5fe15fb5896484c47afe5c0 Mon Sep 17 00:00:00 2001 From: Yann Date: Thu, 21 Apr 2016 09:37:15 +0200 Subject: [PATCH 1/2] Add more capabilities to Model class to handles actives groups --- lodel/editorial_model/model.py | 72 +++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/lodel/editorial_model/model.py b/lodel/editorial_model/model.py index 7d8aae7..d439405 100644 --- a/lodel/editorial_model/model.py +++ b/lodel/editorial_model/model.py @@ -4,6 +4,9 @@ import hashlib import importlib from lodel.utils.mlstring import MlString +from lodel.logger import logger +from lodel.settings import Settings +from lodel.settings.utils import SettingsError from lodel.editorial_model.exceptions import * from lodel.editorial_model.components import EmClass, EmField, EmGroup @@ -21,13 +24,21 @@ class EditorialModel(object): self.__groups = dict() ##@brief Stores all classes indexed by id self.__classes = dict() + ## @brief Stores all activated groups indexed by id + self.__active_groups = dict() + ## @brief Stores all activated classes indexed by id + self.__active_classes = dict() ##@brief EmClass accessor - # @param uid None | str : give this argument to get a specific EmClass - # @return if uid is given returns an EmClass else returns an EmClass iterator + #@param uid None | str : give this argument to get a specific EmClass + #@return if uid is given returns an EmClass else returns an EmClass + # iterator + #@todo use Settings.editorialmodel.groups to determine wich classes should + # be returned def classes(self, uid = None): try: - return self.__elt_getter(self.__classes, uid) + return self.__elt_getter( self.__active_classes, + uid) except KeyError: raise EditorialModelException("EmClass not found : '%s'" % uid) @@ -36,10 +47,34 @@ class EditorialModel(object): # @return if uid is given returns an EmGroup else returns an EmGroup iterator def groups(self, uid = None): try: - return self.__elt_getter(self.__groups, uid) + return self.__elt_getter( self.__active_groups, + uid) except KeyError: raise EditorialModelException("EmGroup not found : '%s'" % uid) + ##@brief Private getter for __groups or __classes + # @see classes() groups() + def __elt_getter(self, elts, uid): + return list(elts.values()) if uid is None else elts[uid] + + ##@brief Update the EditorialModel.__active_groups and + #EditorialModel.__active_classes attibutes + def __set_actives(self): + if Settings.editorialmodel.editormode: + # all groups & classes actives because we are in editor mode + self.__active_groups = self.__groups + self.__active_classes = self.__classes + else: + #determine groups first + self.__active_groups = dict() + for agrp in Settings.editorialmodel.groups: + if agrp not in self.__groups: + raise SettingsError('Invalid group found in settings : %s' % agrp) + grp = self.__groups[agrp] + self.__active_groups[grp.uid] = grp + for acls in grp.components(): + self.__active_classes[acls.uid] = acls + ##@brief EmField getter # @param uid str : An EmField uid represented by "CLASSUID.FIELDUID" # @return Fals or an EmField instance @@ -65,6 +100,7 @@ class EditorialModel(object): # @param emclass EmClass : the EmClass instance to add # @return emclass def add_class(self, emclass): + self.raise_if_ro() if not isinstance(emclass, EmClass): raise ValueError(" expected but got %s " % type(emclass)) if emclass.uid in self.classes(): @@ -76,6 +112,7 @@ class EditorialModel(object): # @param emgroup EmGroup : the EmGroup instance to add # @return emgroup def add_group(self, emgroup): + self.raise_if_ro() if not isinstance(emgroup, EmGroup): raise ValueError(" expected but got %s" % type(emgroup)) if emgroup.uid in self.groups(): @@ -84,25 +121,36 @@ class EditorialModel(object): return emgroup ##@brief Add a new EmClass to the editorial model - # @param uid str : EmClass uid - # @param **kwargs : EmClass constructor options ( see @ref lodel.editorial_model.component.EmClass.__init__() ) + #@param uid str : EmClass uid + #@param **kwargs : EmClass constructor options ( + # see @ref lodel.editorial_model.component.EmClass.__init__() ) def new_class(self, uid, **kwargs): + self.raise_if_ro() return self.add_class(EmClass(uid, **kwargs)) ##@brief Add a new EmGroup to the editorial model - # @param uid str : EmGroup uid - # @param *kwargs : EmGroup constructor keywords arguments (see @ref lodel.editorial_model.component.EmGroup.__init__() ) + #@param uid str : EmGroup uid + #@param *kwargs : EmGroup constructor keywords arguments ( + # see @ref lodel.editorial_model.component.EmGroup.__init__() ) def new_group(self, uid, **kwargs): + self.raise_if_ro() return self.add_group(EmGroup(uid, **kwargs)) - # @brief Save a model + ##@brief Save a model # @param translator module : The translator module to use # @param **translator_args def save(self, translator, **translator_kwargs): + self.raise_if_ro() if isinstance(translator, str): translator = self.translator_from_name(translator) return translator.save(self, **translator_kwargs) + ##@brief Raise an error if lodel is not in EM edition mode + @staticmethod + def raise_if_ro(): + if not Settings.editorialmodel.editormode: + raise EditorialModelError("Lodel in not in EM editor mode. The EM is in read only state") + ##@brief Load a model # @param translator module : The translator module to use # @param **translator_args @@ -125,12 +173,6 @@ class EditorialModel(object): raise NameError("No translator named %s") return mod - - ##@brief Private getter for __groups or __classes - # @see classes() groups() - def __elt_getter(self, elts, uid): - return list(elts.values()) if uid is None else elts[uid] - ##@brief Lodel hash def d_hash(self): payload = "%s%s" % ( From d30f3e189fb6c12df7ac23b7b89e627f550dc708 Mon Sep 17 00:00:00 2001 From: Yann Date: Thu, 21 Apr 2016 09:39:37 +0200 Subject: [PATCH 2/2] [1 test fail] Updated the settings to prepare them to be writable + more tests --- loader.py | 7 - lodel/__init__.py | 4 +- lodel/leapi/datahandlers/base_classes.py | 19 +-- lodel/settings/__init__.py | 11 +- lodel/settings/settings.py | 146 +++++++++--------- lodel/settings/settings_loader.py | 5 +- lodel/settings/validator.py | 14 ++ tests/loader_utils.py | 6 +- .../simple.conf.d/settings.ini | 2 + tests/settings/test_settings.py | 2 + tests/settings/test_settings_loader.py | 23 +++ 11 files changed, 135 insertions(+), 104 deletions(-) delete mode 100644 loader.py diff --git a/loader.py b/loader.py deleted file mode 100644 index 5d75752..0000000 --- a/loader.py +++ /dev/null @@ -1,7 +0,0 @@ -#-*- coding: utf-8 -*- - -import imp -import lodel.settings -from lodel.settings.settings import Settings -Settings.bootstrap(conf_file = 'settings_local.ini', conf_dir = 'globconf.d') -imp.reload(lodel.settings) diff --git a/lodel/__init__.py b/lodel/__init__.py index 16f3a79..84c3a11 100644 --- a/lodel/__init__.py +++ b/lodel/__init__.py @@ -1 +1,3 @@ -__author__ = 'roland' +#-*- coding: utf-8 -*- + +from .utils.starter import init_lodel diff --git a/lodel/leapi/datahandlers/base_classes.py b/lodel/leapi/datahandlers/base_classes.py index f3d2e00..0da7728 100644 --- a/lodel/leapi/datahandlers/base_classes.py +++ b/lodel/leapi/datahandlers/base_classes.py @@ -17,9 +17,9 @@ class FieldValidationError(Exception): ##@brief Base class for all data handlers class DataHandler(object): - __HANDLERS_MODULES = ('datas_base', 'datas', 'references') + _HANDLERS_MODULES = ('datas_base', 'datas', 'references') ##@brief Stores the DataHandler childs classes indexed by name - __base_handlers = None + _base_handlers = None ##@brief Stores custom datahandlers classes indexed by name # @todo do it ! (like plugins, register handlers... blablabla) __custom_handlers = dict() @@ -131,15 +131,15 @@ class DataHandler(object): @classmethod def load_base_handlers(cls): - if cls.__base_handlers is None: - cls.__base_handlers = dict() - for module_name in cls.__HANDLERS_MODULES: + if cls._base_handlers is None: + cls._base_handlers = dict() + for module_name in cls._HANDLERS_MODULES: module = importlib.import_module('lodel.leapi.datahandlers.%s' % module_name) for name, obj in inspect.getmembers(module): if inspect.isclass(obj): logger.debug("Load data handler %s.%s" % (obj.__module__, obj.__name__)) - cls.__base_handlers[name.lower()] = obj - return copy.copy(cls.__base_handlers) + cls._base_handlers[name.lower()] = obj + return copy.copy(cls._base_handlers) ##@brief given a field type name, returns the associated python class # @param fieldtype_name str : A field type name (not case sensitive) @@ -148,10 +148,11 @@ class DataHandler(object): # @note To access custom data handlers it can be cool to preffix the handler name by plugin name for example ? (to ensure name unicity) @classmethod def from_name(cls, name): + cls.load_base_handlers() name = name.lower() - if name not in cls.__base_handlers: + if name not in cls._base_handlers: raise NameError("No data handlers named '%s'" % (name,)) - return cls.__base_handlers[name] + return cls._base_handlers[name] ##@brief Return the module name to import in order to use the datahandler # @param data_handler_name str : Data handler name diff --git a/lodel/settings/__init__.py b/lodel/settings/__init__.py index a002439..120789f 100644 --- a/lodel/settings/__init__.py +++ b/lodel/settings/__init__.py @@ -34,14 +34,5 @@ # # - -import warnings -from lodel.settings.settings import Settings as SettingsHandler - -settings = SettingsHandler.instance - -if settings is None: - Settings = None -else: - Settings = settings.confs.lodel2 +from lodel.settings.settings import SettingsRO as Settings diff --git a/lodel/settings/settings.py b/lodel/settings/settings.py index e15f984..7122984 100644 --- a/lodel/settings/settings.py +++ b/lodel/settings/settings.py @@ -23,6 +23,12 @@ PYTHON_SYS_LIB_PATH = '/usr/local/lib/python{major}.{minor}/'.format( major = sys.version_info.major, minor = sys.version_info.minor) +class MetaSettings(type): + @property + def s(self): + self.singleton_assert(True) + return self.instance.settings + ##@brief Handles configuration load etc. # # To see howto bootstrap Settings and use it in lodel instance see @@ -61,43 +67,57 @@ PYTHON_SYS_LIB_PATH = '/usr/local/lib/python{major}.{minor}/'.format( # '.*') # @todo delete the first stage, the lib path HAVE TO BE HARDCODED. In fact #when we will run lodel in production the lodel2 lib will be in the python path -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')), - } +class Settings(object, metaclass=MetaSettings): + + ## @brief Stores the singleton instance instance = None - ##@brief Should be called only by the boostrap classmethod - # @param conf_file str : Path to the global lodel2 configuration file - # @param conf_dir str : Path to the conf directory - def __init__(self, conf_file, conf_dir): - self.__confs = dict() + ## @brief Instanciate the Settings singleton + # @param conf_dir str : The configuration directory + def __init__(self, conf_dir): + self.singleton_assert() # check that it is the only instance + Settings.instance = self + ## @brief Configuration specification + # + # Initialized by Settings.__bootstrap() method + self.__conf_specs = None + ## @brief Stores the configurations in namedtuple tree + self.__confs = None self.__conf_dir = conf_dir - 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 Stores as class attribute a Settings instance - @classmethod - def bootstrap(cls, conf_file = None, conf_dir = None): - if cls.instance is None: - if conf_file is None and conf_dir is None: - warnings.warn("Lodel instance without settings !!!") - else: - cls.instance = cls(conf_file, conf_dir) - return cls.instance - - ##@brief Configuration keys accessor - # @return All confs organised into named tuples + ## @brief Get the named tuple representing configuration @property - def confs(self): - return copy.copy(self.__confs) + def settings(self): + return self.__confs.lodel2 + + ## @brief Delete the singleton instance + @classmethod + def stop(cls): + del(cls.instance) + cls.instance = None + + @classmethod + def started(cls): + return cls.instance is not None + + ##@brief An utility method that raises if the singleton is not in a good + # state + #@param expect_instanciated bool : if True we expect that the class is + # allready instanciated, else not + # @throw RuntimeError + @classmethod + def singleton_assert(cls, expect_instanciated = False): + if expect_instanciated: + if not cls.started(): + raise RuntimeError("The Settings class is not started yet") + else: + if cls.started(): + raise RuntimeError("The Settings class is allready started") + + @classmethod + def set(cls, confname, confvalue): + pass ##@brief This method handlers Settings instance bootstraping def __bootstrap(self): @@ -108,16 +128,25 @@ class Settings(object): for kname in lodel2_specs[section]: if kname.lower() != kname: raise SettingsError("Only lower case are allowed in section name (thank's ConfigParser...)") - - + + # Load specs for the plugins list and plugins_path list conf keys plugins_opt_specs = lodel2_specs['lodel2']['plugins'] - + plugins_path_opt_specs = lodel2_specs['lodel2']['plugins_path'] # Init the settings loader loader = SettingsLoader(self.__conf_dir) # fetching list of plugins to load - plugins_list = loader.getoption('lodel2', 'plugins', plugins_opt_specs[1], plugins_opt_specs[0], False) + plugins_list = loader.getoption( 'lodel2', + 'plugins', + plugins_opt_specs[1], + plugins_opt_specs[0], + False) + plugins_path = loader.getoption( 'lodel2', + 'plugins_path', + plugins_path_opt_specs[1], + plugins_path_opt_specs[0], + False) # Starting the Plugins class - Plugins.bootstrap(self.__confs['lodel2']['plugins_path']) + Plugins.bootstrap(plugins_path) # Fetching conf specs from plugins specs = [lodel2_specs] errors = list() @@ -128,8 +157,8 @@ class Settings(object): errors.append(e) if len(errors) > 0: #Raise all plugins import errors raise SettingsErrors(errors) - specs = self.__merge_specs(specs) - self.__populate_from_specs(specs, loader) + self.__conf_specs = self.__merge_specs(specs) + self.__populate_from_specs(self.__conf_specs, loader) ##@brief Produce a configuration specification dict by merging all specifications # @@ -159,6 +188,7 @@ class Settings(object): # @param specs dict : Settings specification dictionnary as returned by __merge_specs # @param loader SettingsLoader : A SettingsLoader instance def __populate_from_specs(self, specs, loader): + self.__confs = dict() specs = copy.copy(specs) #Avoid destroying original specs dict (may be useless) # Construct final specs dict replacing variable sections # by the actual existing sections @@ -242,39 +272,11 @@ class Settings(object): ResNamedTuple = namedtuple(name, conftree.keys()) return ResNamedTuple(**conftree) - ##@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") +class MetaSettingsRO(type): + def __getattr__(self, name): + return getattr(Settings.s, name) - #Load default values in result - res = dict() - for keyname, (default, _) in self._conf_preload.items(): - res[keyname] = default - - 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 +## @brief A class that provide . notation read only access to configurations +class SettingsRO(object, metaclass=MetaSettingsRO): + pass diff --git a/lodel/settings/settings_loader.py b/lodel/settings/settings_loader.py index 19fe087..df1ec40 100644 --- a/lodel/settings/settings_loader.py +++ b/lodel/settings/settings_loader.py @@ -58,7 +58,10 @@ class SettingsLoader(object): if keyname in sec: optionstr=sec[keyname] option=validator(sec[keyname]) - del self.__conf_sv[section + ':' + keyname] + try: + del self.__conf_sv[section + ':' + keyname] + except KeyError: #allready fetched + pass return option elif mandatory: raise SettingsError("Default value mandatory for option %s" % keyname) diff --git a/lodel/settings/validator.py b/lodel/settings/validator.py index 31513a1..a069e0f 100644 --- a/lodel/settings/validator.py +++ b/lodel/settings/validator.py @@ -148,6 +148,11 @@ def loglevel_val(value): raise SettingsValidationError("The value '%s' is not a valid loglevel") return value.upper() +def path_val(value): + if not os.path.exists(value): + raise SettingsValidationError("The value '%s' is not a valid path") + return value + # # Default validators registration # @@ -182,6 +187,11 @@ SettingValidator.register_validator( loglevel_val, 'Loglevel validator') +SettingValidator.register_validator( + 'path', + path_val, + 'path validator') + SettingValidator.create_list_validator( 'list', SettingValidator('strip'), @@ -208,10 +218,14 @@ LODEL2_CONF_SPECS = { 'lodel2': { 'debug': ( True, SettingValidator('bool')), + 'plugins_path': ( None, + SettingValidator('list')), 'plugins': ( "", SettingValidator('list')), 'sitename': ( 'noname', SettingValidator('strip')), + 'lib_path': ( None, + SettingValidator('path')), }, 'lodel2.logging.*' : { 'level': ( 'ERROR', diff --git a/tests/loader_utils.py b/tests/loader_utils.py index a0fcc3e..66266d7 100644 --- a/tests/loader_utils.py +++ b/tests/loader_utils.py @@ -4,9 +4,7 @@ # This file should be imported in every tests files # -import imp -import lodel.settings from lodel.settings.settings import Settings -Settings.bootstrap(conf_file = 'settings_local.ini', conf_dir = 'globconf.d') -imp.reload(lodel.settings) +if not Settings.started(): + Settings('globconf.d') diff --git a/tests/settings/settings_examples/simple.conf.d/settings.ini b/tests/settings/settings_examples/simple.conf.d/settings.ini index 58e229d..90c07ad 100644 --- a/tests/settings/settings_examples/simple.conf.d/settings.ini +++ b/tests/settings/settings_examples/simple.conf.d/settings.ini @@ -1,3 +1,5 @@ [lodel2.foo.bar] foo = 42 foobar = hello world +foo_bar = foobar +foo.bar = barfoo diff --git a/tests/settings/test_settings.py b/tests/settings/test_settings.py index ada87cb..075feb7 100644 --- a/tests/settings/test_settings.py +++ b/tests/settings/test_settings.py @@ -3,6 +3,7 @@ import unittest from unittest import mock +""" import tests.loader_utils from lodel.settings.settings import Settings @@ -12,3 +13,4 @@ class SettingsTestCase(unittest.TestCase): def test_init(self): settings = Settings('tests/settings/settings_tests.ini', 'tests/settings/settings_tests_conf.d') pass +""" diff --git a/tests/settings/test_settings_loader.py b/tests/settings/test_settings_loader.py index c943d6c..eee3a57 100644 --- a/tests/settings/test_settings_loader.py +++ b/tests/settings/test_settings_loader.py @@ -58,6 +58,29 @@ class SettingsLoaderTestCase(unittest.TestCase): self.assertEqual(value, "42") value = loader.getoption('lodel2.foo.bar', 'foobar', dummy_validator) self.assertEqual(value, "hello world") + value = loader.getoption('lodel2.foo.bar', 'foo_bar', dummy_validator) + self.assertEqual(value, "foobar") + value = loader.getoption('lodel2.foo.bar', 'foo.bar', dummy_validator) + self.assertEqual(value, "barfoo") + + def test_getoption_multiple_time(self): + """ Testing the behavior when doing 2 time the same call to getoption """ + loader = SettingsLoader('tests/settings/settings_examples/simple.conf.d') + value = loader.getoption('lodel2.foo.bar', 'foo', dummy_validator) + value = loader.getoption('lodel2.foo.bar', 'foo', dummy_validator) + value = loader.getoption('lodel2.foo.bar', 'foo', dummy_validator) + value = loader.getoption('lodel2.foo.bar', 'foo', dummy_validator) + + + def test_geoption_default_value(self): + """ Testing behavior of default value in getoption """ + loader = SettingsLoader('tests/settings/settings_examples/simple.conf.d') + # for non existing keys in file + value = loader.getoption('lodel2.foo.bar', 'foofoofoo', dummy_validator, 'hello 42', False) + self.assertEqual(value, 'hello 42') + # for non existing section in file + value = loader.getoption('lodel2.foofoo', 'foofoofoo', dummy_validator, 'hello 42', False) + self.assertEqual(value, 'hello 42') def test_getoption_complex(self): """ Testing behavior of getoption with less simple files & confs """