Browse Source

Commit with comments, code clean etc.

Yann Weber 8 years ago
parent
commit
c0c5d23adb

+ 14
- 0
lodel/plugin/__init__.py View File

@@ -0,0 +1,14 @@
1
+#-*- coding: utf-8 -*-
2
+
3
+## @page howto_writeplugin Write a plugin howto
4
+#
5
+# @section howto_writeplugin_basicstruct Plugin basic structure
6
+# A plugins is a python package that have to contains 3 files :
7
+# - <code>__init__.py</code>
8
+# - <code>main.py</code> ( defined in @ref lodel.plugin.plugins.MAIN_NAME )
9
+# - <code>confspec.py</code> ( defined in @ref lodel.plugin.plugins.CONFSPEC_NAME )
10
+#
11
+# 
12
+
13
+from .hooks import LodelHook
14
+from .plugins import Plugins

+ 85
- 0
lodel/plugin/hooks.py View File

@@ -0,0 +1,85 @@
1
+#-*- coding: utf-8 -*-
2
+
3
+import os
4
+import copy
5
+from importlib.machinery import SourceFileLoader
6
+
7
+## @brief Class designed to handle a hook's callback with a priority
8
+class DecoratedWrapper(object):
9
+    ## @brief Constructor
10
+    # @param hook function : the function to wrap
11
+    # @param priority int : the callbacl priority
12
+    def __init__(self, hook, priority):
13
+        self._priority = priority
14
+        self._hook = hook
15
+    
16
+    ## @brief Call the callback
17
+    # @param hook_name str : The name of the called hook
18
+    # @param caller * : The caller (depends on the hook)
19
+    # @param payload * : Datas that depends on the hook
20
+    # @return modified payload
21
+    def __call__(self, hook_name, caller, payload):
22
+        return self._hook(hook_name, caller, payload)
23
+
24
+## @brief Decorator designed to register hook's callbacks
25
+#
26
+# @note Decorated functions are expected to take 3 arguments :
27
+#  - hook_name : the called hook name
28
+#  - caller : the hook caller (depends on the hook)
29
+#  - payload : datas depending on the hook
30
+class LodelHook(object):
31
+    
32
+    ## @brief Stores all hooks (DecoratedWrapper instances)
33
+    _hooks = dict()
34
+    
35
+    ## @brief Decorator constructor
36
+    # @param hook_name str : the name of the hook to register to
37
+    # @param priority int : the hook priority
38
+    def __init__(self, hook_name, priority = None):
39
+        self._hook_name = hook_name
40
+        self._priority = 0xFFFF if priority is None else priority
41
+    
42
+    ## @brief called just after __init__
43
+    # @param hook function : the decorated function
44
+    # @return the hook argument
45
+    def __call__(self, hook):
46
+        if not self._hook_name in self._hooks:
47
+            self._hooks[self._hook_name] = list()
48
+        wrapped = DecoratedWrapper(hook, self._priority)
49
+        self._hooks[self._hook_name].append(wrapped)
50
+        self._hooks[self._hook_name] = sorted(self._hooks[self._hook_name], key = lambda h: h._priority)
51
+        return hook
52
+
53
+    ## @brief Call hooks
54
+    # @param hook_name str : the hook's name
55
+    # @param caller * : the hook caller (depends on the hook)
56
+    # @param payload * : datas for the hook
57
+    # @param cls
58
+    # @return modified payload
59
+    @classmethod
60
+    def call_hook(cls, hook_name, caller, payload):
61
+        if hook_name in cls._hooks:
62
+            for hook in cls._hooks[hook_name]:
63
+                payload = hook(hook_name, caller, payload)
64
+        return payload
65
+    
66
+    ## @brief Fetch registered hooks
67
+    # @param names list | None : optionnal filter on name
68
+    # @param cls
69
+    # @return a list of functions
70
+    @classmethod
71
+    def hook_list(cls, names = None):
72
+        res = None
73
+        if names is not None:
74
+            res = { name: copy.copy(cls._hooks[name]) for name in cls._hooks if name in names}
75
+        else:
76
+            res = copy.copy(cls._hooks)
77
+        return { name: [(hook._hook, hook._priority) for hook in hooks] for name, hooks in res.items() }
78
+    
79
+    ## @brief Unregister all hooks
80
+    # @param cls
81
+    # @warning REALLY NOT a good idea !
82
+    # @note implemented for testing purpose
83
+    @classmethod
84
+    def __reset__(cls):
85
+        cls._hooks = dict()

lodel/plugins.py → lodel/plugin/plugins.py View File

@@ -12,8 +12,11 @@ from importlib.machinery import SourceFileLoader, SourcelessFileLoader
12 12
 # - main.py containing hooks registration etc
13 13
 # - confspec.py containing a configuration specification dictionary named CONFSPEC
14 14
 
15
+## @brief The package in wich we will load plugins modules
15 16
 VIRTUAL_PACKAGE_NAME = 'lodel.plugins_pkg'
16
-CONFSPEC_NAME = 'confspec.py'
17
+CONFSPEC_FILENAME = 'confspec.py'
18
+MAIN_FILENAME = 'main.py'
19
+CONFSPEC_VARNAME = 'CONFSPEC'
17 20
 
18 21
 class PluginError(Exception):
19 22
     pass
@@ -25,7 +28,7 @@ class Plugins(object):
25 28
     ## @brief Optimisation cache storage for plugin paths
26 29
     _plugin_paths = dict()
27 30
 
28
-    def __init__(self):
31
+    def __init__(self): # may be useless
29 32
         self.started()
30 33
     
31 34
     ## @brief Given a plugin name returns the plugin path
@@ -59,14 +62,30 @@ class Plugins(object):
59 62
                                     plugin_name)
60 63
         conf_spec_module = plugin_module + '.confspec'
61 64
         
62
-        conf_spec_source = plugin_path + CONFSPEC_NAME
65
+        conf_spec_source = plugin_path + CONFSPEC_FILENAME
63 66
         try:
64 67
             loader = SourceFileLoader(conf_spec_module, conf_spec_source)
65 68
             confspec_module = loader.load_module()
66 69
         except ImportError:
67 70
             raise PluginError("Failed to load plugin '%s'. It seems that the plugin name is not valid" % plugin_name)
68
-        return getattr(confspec_module, 'CONFSPEC')
69
-    
71
+        return getattr(confspec_module, CONFSPEC_VARNAME)
72
+ 
73
+    ## @brief Load a module to register plugin's hooks
74
+    # @param plugin_name str : The plugin name
75
+    @classmethod
76
+    def load_plugin(cls, plugin_name):
77
+        cls.started()
78
+        plugin_path = cls.plugin_path(plugin_name)
79
+        plugin_module = '%s.%s' % ( VIRTUAL_PACKAGE_NAME,
80
+                                    plugin_name)
81
+        main_module = plugin_module + '.main'
82
+        main_source = plugin_path + MAIN_FILENAME
83
+        try:
84
+            loader = SourceFileLoader(main_module, main_source)
85
+            main_module = loader.load_module()
86
+        except ImportError:
87
+            raise PluginError("Failed to load plugin '%s'. It seems that the plugin name is not valid" % plugin_name)
88
+        
70 89
     ## @brief Bootstrap the Plugins class
71 90
     @classmethod
72 91
     def bootstrap(cls, plugins_directories):

+ 34
- 3
lodel/settings/settings.py View File

@@ -6,7 +6,7 @@ import configparser
6 6
 import copy
7 7
 from collections import namedtuple
8 8
 
9
-from lodel.plugins import Plugins, PluginError
9
+from lodel.plugin.plugins import Plugins, PluginError
10 10
 from lodel.settings.utils import SettingsError, SettingsErrors
11 11
 from lodel.settings.validator import SettingValidator, LODEL2_CONF_SPECS
12 12
 from lodel.settings.settings_loader import SettingsLoader
@@ -37,6 +37,32 @@ PYTHON_SYS_LIB_PATH = '/usr/local/lib/python{major}.{minor}/'.format(
37 37
                                                                         minor = sys.version_info.minor)
38 38
 ## @brief Handles configuration load etc.
39 39
 #
40
+# @par Basic usage
41
+# For example if a file defines confs like :
42
+# <pre>
43
+# [super_section]
44
+# super_conf = super_value
45
+# </pre>
46
+# You can access it with :
47
+# <pre> settings_instance.confs.super_section.super_conf </pre>
48
+#
49
+# @par Init sequence
50
+# The initialization sequence is a bit tricky. In fact, plugins adds allowed configuration 
51
+# sections/values, but the list of plugins to load in in... the settings.
52
+# Here is the conceptual presentation of Settings class initialization stages :
53
+#   -# Preloading (sets values like lodel2 library path or the plugins path)
54
+#   -# Ask a @ref lodel.settings.setting_loader.SettingsLoader to load all configurations files
55
+#   -# Fetch the list of plugins in the loaded settings
56
+#   -# Merge plugins settings specification with the global lodel settings specs ( see @ref lodel.plugin )
57
+#   -# Fetch all settings from the merged settings specs
58
+#
59
+# @par Init sequence in practical
60
+# In practice those steps are done by calling a succession of private methods :
61
+#   -# @ref Settings.__bootstrap() ( steps 1 to 3 )
62
+#   -# @ref Settings.__merge_specs() ( step 4 )
63
+#   -# @ref Settings.__populate_from_specs() (step 5)
64
+#   -# And finally @ref Settings.__confs_to_namedtuple()
65
+#
40 66
 # @todo handles default sections for variable sections (sections ending with '.*')
41 67
 class Settings(object):
42 68
     
@@ -52,10 +78,12 @@ class Settings(object):
52 78
         self.__confs = dict()
53 79
         self.__conf_dir = conf_dir
54 80
         self.__load_bootstrap_conf(conf_file)
55
-        # now we should have the self.__confs['lodel2']['plugins_paths'] and
56
-        # self.__confs['lodel2']['lib_path'] set
81
+        #   now we should have the self.__confs['lodel2']['plugins_paths']
82
+        #   and self.__confs['lodel2']['lib_path'] set
57 83
         self.__bootstrap()
58 84
     
85
+    ## @brief Configuration keys accessor
86
+    # @return All confs organised into named tuples
59 87
     @property
60 88
     def confs(self):
61 89
         return copy.copy(self.__confs)
@@ -112,9 +140,12 @@ class Settings(object):
112 140
         # Construct final specs dict replacing variable sections
113 141
         # by the actual existing sections
114 142
         variable_sections = [ section for section in specs if section.endswith('.*') ]
143
+        print("DEBUG VARIABLE SECTIONS : ")
115 144
         for vsec in variable_sections:
116 145
             preffix = vsec[:-2]
146
+            print("PREFFIX =  ", preffix)
117 147
             for section in loader.getsection(preffix, 'default'): #WARNING : hardcoded default section
148
+                print("SECTIONs = ", section)
118 149
                 specs[section] = copy.copy(specs[vsec])
119 150
             del(specs[vsec])
120 151
         # Fetching valuds for sections

+ 2
- 0
plugins/dummy/__init__.py View File

@@ -0,0 +1,2 @@
1
+__author__ = "Lodel2 dev team"
2
+__fullname__ = "Dummy plugin"

+ 17
- 0
plugins/dummy/main.py View File

@@ -0,0 +1,17 @@
1
+#-*- coding: utf-8 -*-
2
+
3
+from lodel.plugin import LodelHook
4
+
5
+## @brief Hook's callback example
6
+@LodelHook('leapi_get_pre')
7
+@LodelHook('leapi_get_post')
8
+@LodelHook('leapi_update_pre')
9
+@LodelHook('leapi_update_post')
10
+@LodelHook('leapi_delete_pre')
11
+@LodelHook('leapi_delete_post')
12
+@LodelHook('leapi_insert_pre')
13
+@LodelHook('leapi_insert_post')
14
+def dummy_callback(hook_name, caller, payload):
15
+    if Lodel.settings.Settings.debug:
16
+        print("\tHook %s\tcaller %s with %s" % (hook_name, caller, payload))
17
+    return payload   

+ 0
- 0
tests/settings/__init__.py View File


+ 3
- 0
tests/settings/settings_tests.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

+ 4
- 0
tests/settings/settings_tests_conf.d/logger.ini View File

@@ -0,0 +1,4 @@
1
+[lodel2.logging.default]
2
+level = DEBUG
3
+context = True
4
+filename = -

+ 3
- 0
tests/settings/settings_tests_conf.d/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

+ 8
- 37
tests/settings/test_settings.py View File

@@ -1,42 +1,13 @@
1 1
 #-*- coding: utf-8 -*-
2 2
 
3 3
 import unittest
4
-from lodel.settings.settings_loader import SettingsLoader
4
+from unittest import mock
5 5
 
6
-class SettingsLoaderTestCase(unittest.TestCase):
6
+from lodel.settings.settings import Settings
7 7
 
8
-    def test_merge_getsection(self):
9
-        """Tests merge and getSection functions """
10
-        settings = SettingsLoader('/home/helene/lodel2/tests/settings/conf.d')
11
-        a = settings.getsection('A')
12
-        self.assertEqual(a,dict({"a":"a1","b":"b1,b2,b3","c":"toto","fhui":"njl"}))
13
-        b = settings.getsection('B')
14
-        self.assertEqual(b,dict({"ab":"art","bb":"bj,kl,mn","cb":"tatat"}))
15
-        c = settings.getsection('C')
16
-        self.assertEqual(c,dict({"ca":"a2","cb":"b4,b2,b3","cc":"titi"}))
17
-        d = settings.getsection('D')
18
-        
19
-        for v in a:
20
-            assert ('A','{"a":"a1","b":"b1,b2,b3","c":"toto","fhui":"njl"}')
21
-        def maFonction(a):
22
-            return a
23
-        e=settings.getoption('A','a',maFonction)
24
-        self.assertEqual(e,'a1')
25
-        f=settings.getoption('B','bb',maFonction)
26
-        self.assertEqual(f,"bj,kl,mn")
27
-        g=settings.getremains()
28
-        self.assertIsNotNone(g)
29
-        e=settings.getoption('A','b',maFonction)
30
-        e=settings.getoption('A','c',maFonction)
31
-        e=settings.getoption('A','fhui',maFonction)
32
-        f=settings.getoption('B','ab',maFonction)
33
-        f=settings.getoption('B','cb',maFonction)
34
-        f=settings.getoption('C','cb',maFonction)
35
-        f=settings.getoption('C','ca',maFonction)
36
-        f=settings.getoption('C','cc',maFonction)
37
-       
38
-        g=settings.getremains()
39
-        self.assertEqual(g,[])
40
-        
41
-        
42
-        
8
+class SettingsTestCase(unittest.TestCase):
9
+    
10
+    def test_init(self):
11
+        settings = Settings('tests/settings/settings_tests.ini', 'tests/settings/settings_tests_conf.d')
12
+        print(settings.confs)
13
+        pass

+ 42
- 0
tests/settings/test_settings_loader.py View File

@@ -0,0 +1,42 @@
1
+#-*- coding: utf-8 -*-
2
+
3
+import unittest
4
+from lodel.settings.settings_loader import SettingsLoader
5
+
6
+class SettingsLoaderTestCase(unittest.TestCase):
7
+
8
+    def test_merge_getsection(self):
9
+        """Tests merge and getSection functions """
10
+        settings = SettingsLoader('/home/helene/lodel2/tests/settings/conf.d')
11
+        a = settings.getsection('A')
12
+        self.assertEqual(a,dict({"a":"a1","b":"b1,b2,b3","c":"toto","fhui":"njl"}))
13
+        b = settings.getsection('B')
14
+        self.assertEqual(b,dict({"ab":"art","bb":"bj,kl,mn","cb":"tatat"}))
15
+        c = settings.getsection('C')
16
+        self.assertEqual(c,dict({"ca":"a2","cb":"b4,b2,b3","cc":"titi"}))
17
+        d = settings.getsection('D')
18
+        
19
+        for v in a:
20
+            assert ('A','{"a":"a1","b":"b1,b2,b3","c":"toto","fhui":"njl"}')
21
+        def maFonction(a):
22
+            return a
23
+        e=settings.getoption('A','a',maFonction)
24
+        self.assertEqual(e,'a1')
25
+        f=settings.getoption('B','bb',maFonction)
26
+        self.assertEqual(f,"bj,kl,mn")
27
+        g=settings.getremains()
28
+        self.assertIsNotNone(g)
29
+        e=settings.getoption('A','b',maFonction)
30
+        e=settings.getoption('A','c',maFonction)
31
+        e=settings.getoption('A','fhui',maFonction)
32
+        f=settings.getoption('B','ab',maFonction)
33
+        f=settings.getoption('B','cb',maFonction)
34
+        f=settings.getoption('C','cb',maFonction)
35
+        f=settings.getoption('C','ca',maFonction)
36
+        f=settings.getoption('C','cc',maFonction)
37
+       
38
+        g=settings.getremains()
39
+        self.assertEqual(g,[])
40
+        
41
+        
42
+        

Loading…
Cancel
Save