Browse Source

[Broken state] started settings implementation

Yann Weber 8 years ago
parent
commit
68a27ff5dd

+ 72
- 0
lodel/plugins.py View File

@@ -0,0 +1,72 @@
1
+#-*- coding: utf-8 -*-
2
+
3
+import os.path
4
+
5
+from importlib.machinery import SourceFileLoader, SourcelessFileLoader
6
+
7
+## @package lodel.plugins Lodel2 plugins management
8
+#
9
+# Lodel2 plugins are stored in directories
10
+# A typicall lodel2 plugin directory structure looks like :
11
+# - {{__init__.py}}} containing informations like full_name, authors, licence etc.
12
+# - main.py containing hooks registration etc
13
+# - confspec.py containing a configuration specification dictionary named CONFSPEC
14
+
15
+VIRTUAL_PACKAGE_NAME = 'lodel.plugins_pkg'
16
+CONFSPEC_NAME = 'confspec.py'
17
+
18
+class Plugins(object):
19
+    
20
+    ## @brief Stores plugin directories paths
21
+    _plugin_directories = None
22
+    ## @brief Optimisation cache storage for plugin paths
23
+    _plugin_paths = dict()
24
+
25
+    def __init__(self):
26
+        self.started()
27
+    
28
+    ## @brief Given a plugin name returns the plugin path
29
+    # @param plugin_name str : The plugin name
30
+    # @return the plugin directory path
31
+    @classmethod
32
+    def plugin_path(cls, plugin_name):
33
+        cls.started()
34
+        try:
35
+            return cls._plugin_paths[plugin_name]
36
+        except KeyError:
37
+            pass
38
+        
39
+        path = None
40
+        for cur_path in cls._plugin_directories:
41
+            plugin_path = os.path.join(cur_path, plugin_name)+'/'
42
+            print(plugin_path)
43
+            if os.path.isdir(plugin_path):
44
+                return plugin_path
45
+        raise NameError("No plugin named '%s'" % plugin_name)
46
+
47
+    ## @brief Fetch a confspec given a plugin_name
48
+    # @param plugin_name str : The plugin name
49
+    # @return a dict of conf spec
50
+    @classmethod
51
+    def get_confspec(cls, plugin_name):
52
+        cls.started()
53
+        plugin_path = cls.plugin_path(plugin_name)
54
+        plugin_module = '%s.%s' % ( VIRTUAL_PACKAGE_NAME,
55
+                                    plugin_name)
56
+        conf_spec_module = plugin_module + '.confspec'
57
+        
58
+        conf_spec_source = plugin_path + CONFSPEC_NAME
59
+
60
+        loader = SourceFileLoader(conf_spec_module, conf_spec_source)
61
+        confspec_module = loader.load_module()
62
+        return getattr(confspec_module, 'CONFSPEC')
63
+
64
+    @classmethod
65
+    def bootstrap(cls, plugins_directories):
66
+        cls._plugin_directories = plugins_directories
67
+
68
+    @classmethod
69
+    def started(cls, raise_if_not = True):
70
+        res = cls._plugin_directories is not None
71
+        if raise_if_not and not res:
72
+            raise RuntimeError("Class Plugins is not initialized")

+ 0
- 145
lodel/settings.py View File

@@ -1,145 +0,0 @@
1
-#-*- coding: utf-8 -*-
2
-
3
-import types
4
-import warnings
5
-from . import settings_format
6
-
7
-## @package Lodel.settings
8
-#
9
-# @brief Defines stuff to handles Lodel2 configuration (see @ref lodel_settings )
10
-#
11
-# To access the confs use the Lodel.settings.Settings SettingsHandler instance
12
-
13
-## @brief A class designed to handles Lodel2 settings
14
-#
15
-# When instanciating a SettingsHandler, the new instance is filled with the content of settings.py (in the root directory of lodel2
16
-#
17
-# @warning You don't have to instanciate this class, you can access to the global instance with the Settings variable in this module
18
-# @todo broken stuff... Rewrite it
19
-# @todo Forbid module assignement in settings ! and disable tests about this
20
-# @todo Implements a type checking of config value
21
-# @todo Implements default values for config keys
22
-class SettingsHandler(object):
23
-    
24
-    ## @brief Shortcut
25
-    _allowed = settings_format.ALLOWED + settings_format.MANDATORY
26
-    ## @brief Shortcut
27
-    _mandatory = settings_format.MANDATORY
28
-
29
-    def __init__(self):
30
-        try:
31
-            import settings as default_settings
32
-            self._load_module(default_settings)
33
-        except ImportError:
34
-            warnings.warn("Unable to find global default settings")
35
-
36
-        ## @brief A flag set to True when the instance is fully loaded
37
-        self._set_loaded(False if len(self._missings()) > 0 else True)
38
-    
39
-    ## @brief Compat wrapper for getattr
40
-    def get(self, name):
41
-        return getattr(self, name)
42
-    
43
-    ## @brief Compat wrapper for setattr
44
-    def set(self, name, value):
45
-        return setattr(self, name, value)
46
-
47
-    ## @brief Load every module properties in the settings instance
48
-    #
49
-    # Load a module content into a SettingsHandler instance and checks that no mandatory settings are missing
50
-    # @note Example : <pre> import my_cool_settings;
51
-    # Settings._load_module(my_cool_settings);</pre>
52
-    # @param module module|None: a loaded module (if None just check for missing settings)
53
-    # @throw LookupError if invalid settings found or if mandatory settings are missing
54
-    def load_module(self, module = None):
55
-        if not(module is None):
56
-            self._load_module(module)
57
-        missings = self._missings()
58
-        if len(missings) > 0:
59
-            self._loaded = False
60
-            raise LookupError("Mandatory settings are missing : %s"%missings)
61
-        self._set_loaded(True)
62
-    
63
-    ## @brief supersede of default __setattr__ method
64
-    def __setattr__(self, name, value):
65
-        if not hasattr(self, name):
66
-            if name not in self._allowed:
67
-                raise LookupError("Invalid setting : %s"%name)
68
-        super().__setattr__(name, value)
69
-
70
-    ## @brief This method do the job for SettingsHandler.load_module()
71
-    #
72
-    # @note The difference with SettingsHandler.load_module() is that it didn't check if some settings are missing
73
-    # @throw LokkupError if an invalid settings is given
74
-    # @param module : a loaded module
75
-    def _load_module(self, module):
76
-        errors = []
77
-        fatal_errors = []
78
-        conf_dict = {
79
-            name: getattr(module, name)
80
-            for name in dir(module) 
81
-            if not name.startswith('__') and not isinstance(getattr(module, name), types.ModuleType)
82
-        }
83
-        for name, value in conf_dict.items():
84
-            try:
85
-                setattr(self, name, value)
86
-            except LookupError:
87
-                errors.append(name)
88
-        if len(errors) > 0:
89
-            err_msg = "Found invalid settings in %s : %s"%(module.__name__, errors)
90
-            raise LookupError(err_msg)
91
-
92
-    ## @brief Refresh the allowed and mandatory settings list
93
-    @classmethod
94
-    def _refresh_format(cls):
95
-        ## @brief Shortcut
96
-        cls._allowed = settings_format.ALLOWED + settings_format.MANDATORY
97
-        ## @brief Shortcut
98
-        cls._mandatory = settings_format.MANDATORY
99
-
100
-    ## @brief If some settings are missings return their names
101
-    # @return an array of string
102
-    def _missings(self):
103
-        return [ confname for confname in self._mandatory if not hasattr(self, confname) ]
104
-
105
-    def _set_loaded(self, value):
106
-        super().__setattr__('_loaded', bool(value))
107
-
108
-Settings = SettingsHandler()
109
-
110
-## @page lodel_settings Lodel SettingsHandler
111
-#
112
-# This page describe the way settings are handled in Lodel2.
113
-#
114
-# @section lodel_settings_files Lodel settings files
115
-#
116
-# - Lodel/settings.py defines the Lodel.settings package, the SettingsHandler class and the Lodel.settings.Settings instance
117
-# - Lodel/settings_format.py defines the mandatory and allowed configurations keys lists
118
-# - install/instance_settings.py is a model of the file that will be deployed in Lodel2 instances directories
119
-#
120
-# @section Using Lodel.settings.Settings SettingsHandler instance
121
-#
122
-# @subsection lodel_settings_without_loader Without loader
123
-#
124
-# Without any loader you can import Lodel.settings.Settings and acces its property with getattr (or . ) or with SettingsHandler.get() method.
125
-# In the same way you can set a settings by standart affectation of a propery or with SettingsHandler.set() method.
126
-#
127
-# @subsection lodel_settings_loader With a loader in a lodel2 instance
128
-#
129
-# 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
130
-#
131
-# @subsection lodel_settings_example Examples
132
-#
133
-# <pre>
134
-# #!/usr/bin/python
135
-# from Lodel.settings import Settings
136
-# if Settings.debug:
137
-#   print("DEBUG")
138
-# # or
139
-# if Settings.get('debug'):
140
-#   print("DEBUG")
141
-# Settings.debug = False
142
-# # or
143
-# Settings.set('debug', False)
144
-# </pre>
145
-# 

+ 102
- 0
lodel/settings/settings.py View File

@@ -0,0 +1,102 @@
1
+#-*- coding: utf-8 -*-
2
+
3
+import sys
4
+import os
5
+import configparser
6
+
7
+from lodel.plugins import Plugins
8
+from lodel.settings.utils import SettingsError, SettingsErrors
9
+from lodel.settings.validator import SettingValidator
10
+from lodel.settings.settings_loader import SettingsLoader
11
+
12
+## @package lodel.settings Lodel2 settings package
13
+#
14
+# Contains all module that help handling settings
15
+
16
+## @package lodel.settings.settings Lodel2 settings module
17
+#
18
+# Handles configuration load/parse/check.
19
+#
20
+# @subsection Configuration load process
21
+#
22
+# The configuration load process is not trivial. In fact loaded plugins are able to add their own options.
23
+# But the list of plugins to load and the plugins options are in the same file, the instance configuration file.
24
+#
25
+# @subsection Configuration specification
26
+#
27
+# Configuration specification is divided in 2 parts :
28
+# - default values
29
+# - value validation/cast (see @ref Lodel.settings.validator.ConfValidator )
30
+# 
31
+
32
+PYTHON_SYS_LIB_PATH = '/usr/local/lib/python{major}.{minor}/'.format(
33
+
34
+                                                                        major = sys.version_info.major,
35
+                                                                        minor = sys.version_info.minor)
36
+## @brief Handles configuration load etc.
37
+class Settings(object):
38
+    
39
+    ## @brief global conf specsification (default_value + validator)
40
+    _conf_preload = {
41
+            'lib_path': (   PYTHON_SYS_LIB_PATH+'/lodel2/',
42
+                            SettingValidator('directory')),
43
+            'plugins_path': (   PYTHON_SYS_LIB_PATH+'lodel2/plugins/',
44
+                                SettingValidator('directory_list')),
45
+    }
46
+    
47
+    def __init__(self, conf_file = '/etc/lodel2/lodel2.conf', conf_dir = 'conf.d'):
48
+        self.__confs = dict()
49
+        
50
+        self.__load_bootstrap_conf(conf_file)
51
+        # now we should have the self.__confs['lodel2']['plugins_paths'] and
52
+        # self.__confs['lodel2']['lib_path'] set
53
+        self.__bootstrap()
54
+    
55
+    ## @brief This method handlers Settings instance bootstraping
56
+    def __bootstrap(self):
57
+        #loader = SettingsLoader(self.__conf_dir)
58
+
59
+        # Starting the Plugins class
60
+        Plugins.bootstrap(self.__confs['lodel2']['plugins_path'])
61
+        specs = Plugins.get_confspec('dummy')
62
+        print("Got specs : %s " % specs)
63
+        
64
+        # then fetch options values from conf specs
65
+    
66
+    ## @brief Load base global configurations keys
67
+    #
68
+    # Base configurations keys are :
69
+    # - lodel2 lib path
70
+    # - lodel2 plugins path
71
+    #
72
+    # @note return nothing but set the __confs attribute
73
+    # @see Settings._conf_preload
74
+    def __load_bootstrap_conf(self, conf_file):
75
+        config = configparser.ConfigParser()
76
+        config.read(conf_file)
77
+        sections = config.sections()
78
+        if len(sections) != 1 or sections[0].lower() != 'lodel2':
79
+            raise SettingsError("Global conf error, expected lodel2 section not found")
80
+        
81
+        #Load default values in result
82
+        res = dict()
83
+        for keyname, (keyvalue, validator) in self._conf_preload.items():
84
+            res[keyname] = keyvalue
85
+
86
+        confs = config[sections[0]]
87
+        errors = []
88
+        for name in confs:
89
+            if name not in res:
90
+                errors.append(  SettingsError(
91
+                                    "Unknow field",
92
+                                    "lodel2.%s" % name,
93
+                                    conf_file))
94
+            try:
95
+                res[name] = self._conf_preload[name][1](confs[name])
96
+            except Exception as e:
97
+                errors.append(SettingsError(str(e), name, conf_file))
98
+        if len(errors) > 0:
99
+            raise SettingsErrors(errors)
100
+        
101
+        self.__confs['lodel2'] = res
102
+

+ 19
- 0
lodel/settings/utils.py View File

@@ -22,4 +22,23 @@ class SettingsError(Exception):
22 22
 
23 23
         res += ": %s" % (self.__msg)
24 24
         return res
25
+
26
+## @brief Designed to handles mutliple SettingsError
27
+class SettingsErrors(Exception):
28
+    
29
+    ## @brief Instanciate an SettingsErrors
30
+    # @param exceptions list : list of SettingsError instance
31
+    def __init__(self, exceptions):
32
+        for expt in exceptions: 
33
+            if not isinstance(expt, SettingsError):
34
+                raise ValueError("The 'exceptions' argument has to be an array of <class SettingsError>, but a %s was found in the list" % type(expt))
35
+        self.__exceptions = exceptions
25 36
         
37
+
38
+    def __repr__(self): return str(self)
39
+
40
+    def __str__(self):
41
+        res = "Errors :\n"
42
+        for expt in self.__exceptions:
43
+            res += "\t%s\n" % str(expt)
44
+        return res

+ 181
- 0
lodel/settings/validator.py View File

@@ -0,0 +1,181 @@
1
+#-*- coding: utf-8 -*-
2
+
3
+import sys
4
+import os.path
5
+import re
6
+import inspect
7
+import copy
8
+
9
+## @package lodel.settings.validator Lodel2 settings validators/cast module
10
+#
11
+# Validator are registered in the SettingValidator class.
12
+
13
+class SettingsValidationError(Exception):
14
+    pass
15
+
16
+## @brief Handles settings validators
17
+#
18
+# Class instance are callable objects that takes a value argument (the value to validate). It raises
19
+# a SettingsValidationError if validation fails, else it returns a properly
20
+# casted value.
21
+class SettingValidator(object):
22
+    
23
+    _validators = dict()
24
+    _description = dict()
25
+    
26
+    ## @brief Instanciate a validator
27
+    def __init__(self, name, none_is_valid = False):
28
+        if name is not None and name not in self._validators:
29
+            raise NameError("No validator named '%s'" % name)
30
+        self.__name = name
31
+
32
+    ## @brief Call the validator
33
+    # @param value *
34
+    # @return properly casted value
35
+    # @throw SettingsValidationError
36
+    def __call__(self, value):
37
+        if self.__name is None:
38
+            return value
39
+        try:
40
+            return self._validators[self.__name](value)
41
+        except Exception as e:
42
+            raise SettingsValidationError(e)
43
+    
44
+    ## @brief Register a new validator
45
+    # @param name str : validator name
46
+    # @param callback callable : the function that will validate a value
47
+    @classmethod
48
+    def register_validator(cls, name, callback, description=None):
49
+        if name in cls._validators:
50
+            raise NameError("A validator named '%s' allready exists" % name)
51
+        # Broken test for callable
52
+        if not inspect.isfunction(callback) and not inspect.ismethod(callback) and not hasattr(callback, '__call__'):
53
+            raise TypeError("Callable expected but got %s" % type(callback))
54
+        cls._validators[name] = callback
55
+        cls._description[name] = description
56
+    
57
+    ## @brief Get the validator list associated with description
58
+    @classmethod
59
+    def validators_list(cls):
60
+        return copy.copy(cls._description)
61
+
62
+    ## @brief Create and register a list validator
63
+    # @param elt_validator callable : The validator that will be used for validate each elt value
64
+    # @param validator_name str
65
+    # @param description None | str
66
+    # @param separator str : The element separator
67
+    # @return A SettingValidator instance
68
+    @classmethod
69
+    def create_list_validator(cls, validator_name, elt_validator, description = None, separator = ','):
70
+        def list_validator(value):
71
+            res = list()
72
+            errors = list()
73
+            for elt in value.split(separator):
74
+                res.append(elt_validator(elt))
75
+            return res
76
+        description = "Convert value to an array" if description is None else description
77
+        cls.register_validator(
78
+                                validator_name,
79
+                                list_validator,
80
+                                description)
81
+        return cls(validator_name)
82
+                
83
+    ## @brief Create and register a regular expression validator
84
+    # @param pattern str : regex pattern
85
+    # @param validator_name str : The validator name
86
+    # @param description str : Validator description
87
+    # @return a SettingValidator instance
88
+    @classmethod
89
+    def create_re_validator(cls, pattern, validator_name, description = None):
90
+        def re_validator(value):
91
+            if not re.match(pattern, value):
92
+                raise SettingsValidationError("The value '%s' doesn't match the following pattern '%s'" % pattern)
93
+            return value
94
+        #registering the validator
95
+        cls.register_validator(
96
+                                validator_name,
97
+                                re_validator,
98
+                                ("Match value to '%s'" % pattern) if description is None else description)
99
+        return cls(validator_name)
100
+
101
+    
102
+    ## @return a list of registered validators
103
+    def validators_list_str(cls):
104
+        result = ''
105
+        for name in cls._validators:
106
+            result += "\t%s" % name
107
+            if name in self._description and self._description[name] is not None:
108
+                result += "\t: %s" % self._description[name]
109
+            result += "\n"
110
+        return result
111
+
112
+## @brief Integer value validator callback
113
+def int_val(value):
114
+    return int(value)
115
+
116
+## @brief Output file validator callback
117
+# @return A file object (if filename is '-' return sys.stderr)
118
+def file_err_output(value):
119
+    if not isinstance(value, str):
120
+        raise SettingsValidationError("A string was expected but got '%s' " % value)
121
+    if value == '-':
122
+        return sys.stderr
123
+    return value
124
+
125
+## @brief Boolean value validator callback
126
+def boolean_val(value):
127
+    if not (value is True) and not (value is False):
128
+        raise SettingsValidationError("A boolean was expected but got '%s' " % value)
129
+    return bool(value)
130
+
131
+def directory_val(value):
132
+    res = SettingValidator('strip')(value)
133
+    if not os.path.isdir(res):
134
+        raise SettingsValidationError("Folowing path don't exists or is not a directory : '%s'"%res)
135
+    return res
136
+
137
+#
138
+#   Default validators registration
139
+#
140
+
141
+SettingValidator.register_validator(
142
+                                        'strip',
143
+                                        str.strip,
144
+                                        'String trim')
145
+
146
+SettingValidator.register_validator(
147
+                                        'int',
148
+                                        int_val,
149
+                                        'Integer value validator')
150
+
151
+SettingValidator.register_validator(
152
+                                        'bool',
153
+                                        boolean_val,
154
+                                        'Boolean value validator')
155
+
156
+SettingValidator.register_validator(
157
+                                        'errfile',
158
+                                        file_err_output,
159
+                                        'Error output file validator (return stderr if filename is "-")')
160
+
161
+SettingValidator.register_validator(
162
+                                        'directory',
163
+                                        directory_val,
164
+                                        'Directory path validator')
165
+
166
+SettingValidator.create_list_validator(
167
+                                            'list',
168
+                                            SettingValidator('strip'),
169
+                                            description = "Simple list validator. Validate a list of values separated by ','",
170
+                                            separator = ',')
171
+
172
+SettingValidator.create_list_validator(
173
+                                            'directory_list',
174
+                                            SettingValidator('directory'),
175
+                                            description = "Validator for a list of directory path separated with ','",
176
+                                            separator = ',')
177
+
178
+SettingValidator.create_re_validator(
179
+                                        r'^https?://[^\./]+.[^\./]+/?.*$',
180
+                                        'http_url',
181
+                                        'Url validator')

+ 0
- 26
lodel/settings_format.py View File

@@ -1,26 +0,0 @@
1
-#-*- coding: utf-8 -*-
2
-## @package Lodel.settings_format Rules for settings
3
-
4
-## @brief List mandatory configurations keys
5
-MANDATORY = [
6
-    'debug',
7
-    'debug_sql',
8
-    'sitename',
9
-    'lodel2_lib_path',
10
-    'em_file',
11
-    'dynamic_code_file',
12
-    'ds_package',
13
-    'datasource',
14
-    'mh_classname',
15
-    'migration_options',
16
-    'base_path',
17
-    'plugins',
18
-    'logging',
19
-]
20
-
21
-## @brief List allowed (but not mandatory) configurations keys
22
-ALLOWED = [
23
-    'em_graph_output',
24
-    'em_graph_format',
25
-    'templates_base_dir'
26
-]

+ 7
- 0
plugins/dummy/confspec.py View File

@@ -0,0 +1,7 @@
1
+#-*- coding: utf-8 -*-
2
+
3
+CONFSPEC = {
4
+    'section1': {
5
+        'key1': None
6
+    }
7
+}

+ 3
- 0
settings.ini View File

@@ -0,0 +1,3 @@
1
+[lodel2]
2
+lib_path = /home/yannweb/dev/lodel2/lodel2-git
3
+plugins_path = /home/yannweb/dev/lodel2/lodel2-git/plugins

Loading…
Cancel
Save