mirror of
https://github.com/yweber/lodel2.git
synced 2026-03-23 19:42:02 +01:00
Commit with comments, code clean etc.
This commit is contained in:
parent
8156104d1d
commit
c0c5d23adb
12 changed files with 236 additions and 45 deletions
14
lodel/plugin/__init__.py
Normal file
14
lodel/plugin/__init__.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
## @page howto_writeplugin Write a plugin howto
|
||||
#
|
||||
# @section howto_writeplugin_basicstruct Plugin basic structure
|
||||
# A plugins is a python package that have to contains 3 files :
|
||||
# - <code>__init__.py</code>
|
||||
# - <code>main.py</code> ( defined in @ref lodel.plugin.plugins.MAIN_NAME )
|
||||
# - <code>confspec.py</code> ( defined in @ref lodel.plugin.plugins.CONFSPEC_NAME )
|
||||
#
|
||||
#
|
||||
|
||||
from .hooks import LodelHook
|
||||
from .plugins import Plugins
|
||||
85
lodel/plugin/hooks.py
Normal file
85
lodel/plugin/hooks.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import copy
|
||||
from importlib.machinery import SourceFileLoader
|
||||
|
||||
## @brief Class designed to handle a hook's callback with a priority
|
||||
class DecoratedWrapper(object):
|
||||
## @brief Constructor
|
||||
# @param hook function : the function to wrap
|
||||
# @param priority int : the callbacl priority
|
||||
def __init__(self, hook, priority):
|
||||
self._priority = priority
|
||||
self._hook = hook
|
||||
|
||||
## @brief Call the callback
|
||||
# @param hook_name str : The name of the called hook
|
||||
# @param caller * : The caller (depends on the hook)
|
||||
# @param payload * : Datas that depends on the hook
|
||||
# @return modified payload
|
||||
def __call__(self, hook_name, caller, payload):
|
||||
return self._hook(hook_name, caller, payload)
|
||||
|
||||
## @brief Decorator designed to register hook's callbacks
|
||||
#
|
||||
# @note Decorated functions are expected to take 3 arguments :
|
||||
# - hook_name : the called hook name
|
||||
# - caller : the hook caller (depends on the hook)
|
||||
# - payload : datas depending on the hook
|
||||
class LodelHook(object):
|
||||
|
||||
## @brief Stores all hooks (DecoratedWrapper instances)
|
||||
_hooks = dict()
|
||||
|
||||
## @brief Decorator constructor
|
||||
# @param hook_name str : the name of the hook to register to
|
||||
# @param priority int : the hook priority
|
||||
def __init__(self, hook_name, priority = None):
|
||||
self._hook_name = hook_name
|
||||
self._priority = 0xFFFF if priority is None else priority
|
||||
|
||||
## @brief called just after __init__
|
||||
# @param hook function : the decorated function
|
||||
# @return the hook argument
|
||||
def __call__(self, hook):
|
||||
if not self._hook_name in self._hooks:
|
||||
self._hooks[self._hook_name] = list()
|
||||
wrapped = DecoratedWrapper(hook, self._priority)
|
||||
self._hooks[self._hook_name].append(wrapped)
|
||||
self._hooks[self._hook_name] = sorted(self._hooks[self._hook_name], key = lambda h: h._priority)
|
||||
return hook
|
||||
|
||||
## @brief Call hooks
|
||||
# @param hook_name str : the hook's name
|
||||
# @param caller * : the hook caller (depends on the hook)
|
||||
# @param payload * : datas for the hook
|
||||
# @param cls
|
||||
# @return modified payload
|
||||
@classmethod
|
||||
def call_hook(cls, hook_name, caller, payload):
|
||||
if hook_name in cls._hooks:
|
||||
for hook in cls._hooks[hook_name]:
|
||||
payload = hook(hook_name, caller, payload)
|
||||
return payload
|
||||
|
||||
## @brief Fetch registered hooks
|
||||
# @param names list | None : optionnal filter on name
|
||||
# @param cls
|
||||
# @return a list of functions
|
||||
@classmethod
|
||||
def hook_list(cls, names = None):
|
||||
res = None
|
||||
if names is not None:
|
||||
res = { name: copy.copy(cls._hooks[name]) for name in cls._hooks if name in names}
|
||||
else:
|
||||
res = copy.copy(cls._hooks)
|
||||
return { name: [(hook._hook, hook._priority) for hook in hooks] for name, hooks in res.items() }
|
||||
|
||||
## @brief Unregister all hooks
|
||||
# @param cls
|
||||
# @warning REALLY NOT a good idea !
|
||||
# @note implemented for testing purpose
|
||||
@classmethod
|
||||
def __reset__(cls):
|
||||
cls._hooks = dict()
|
||||
|
|
@ -12,8 +12,11 @@ from importlib.machinery import SourceFileLoader, SourcelessFileLoader
|
|||
# - main.py containing hooks registration etc
|
||||
# - confspec.py containing a configuration specification dictionary named CONFSPEC
|
||||
|
||||
## @brief The package in wich we will load plugins modules
|
||||
VIRTUAL_PACKAGE_NAME = 'lodel.plugins_pkg'
|
||||
CONFSPEC_NAME = 'confspec.py'
|
||||
CONFSPEC_FILENAME = 'confspec.py'
|
||||
MAIN_FILENAME = 'main.py'
|
||||
CONFSPEC_VARNAME = 'CONFSPEC'
|
||||
|
||||
class PluginError(Exception):
|
||||
pass
|
||||
|
|
@ -25,7 +28,7 @@ class Plugins(object):
|
|||
## @brief Optimisation cache storage for plugin paths
|
||||
_plugin_paths = dict()
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self): # may be useless
|
||||
self.started()
|
||||
|
||||
## @brief Given a plugin name returns the plugin path
|
||||
|
|
@ -59,14 +62,30 @@ class Plugins(object):
|
|||
plugin_name)
|
||||
conf_spec_module = plugin_module + '.confspec'
|
||||
|
||||
conf_spec_source = plugin_path + CONFSPEC_NAME
|
||||
conf_spec_source = plugin_path + CONFSPEC_FILENAME
|
||||
try:
|
||||
loader = SourceFileLoader(conf_spec_module, conf_spec_source)
|
||||
confspec_module = loader.load_module()
|
||||
except ImportError:
|
||||
raise PluginError("Failed to load plugin '%s'. It seems that the plugin name is not valid" % plugin_name)
|
||||
return getattr(confspec_module, 'CONFSPEC')
|
||||
|
||||
return getattr(confspec_module, CONFSPEC_VARNAME)
|
||||
|
||||
## @brief Load a module to register plugin's hooks
|
||||
# @param plugin_name str : The plugin name
|
||||
@classmethod
|
||||
def load_plugin(cls, plugin_name):
|
||||
cls.started()
|
||||
plugin_path = cls.plugin_path(plugin_name)
|
||||
plugin_module = '%s.%s' % ( VIRTUAL_PACKAGE_NAME,
|
||||
plugin_name)
|
||||
main_module = plugin_module + '.main'
|
||||
main_source = plugin_path + MAIN_FILENAME
|
||||
try:
|
||||
loader = SourceFileLoader(main_module, main_source)
|
||||
main_module = loader.load_module()
|
||||
except ImportError:
|
||||
raise PluginError("Failed to load plugin '%s'. It seems that the plugin name is not valid" % plugin_name)
|
||||
|
||||
## @brief Bootstrap the Plugins class
|
||||
@classmethod
|
||||
def bootstrap(cls, plugins_directories):
|
||||
|
|
@ -6,7 +6,7 @@ import configparser
|
|||
import copy
|
||||
from collections import namedtuple
|
||||
|
||||
from lodel.plugins import Plugins, PluginError
|
||||
from lodel.plugin.plugins import Plugins, PluginError
|
||||
from lodel.settings.utils import SettingsError, SettingsErrors
|
||||
from lodel.settings.validator import SettingValidator, LODEL2_CONF_SPECS
|
||||
from lodel.settings.settings_loader import SettingsLoader
|
||||
|
|
@ -37,6 +37,32 @@ PYTHON_SYS_LIB_PATH = '/usr/local/lib/python{major}.{minor}/'.format(
|
|||
minor = sys.version_info.minor)
|
||||
## @brief Handles configuration load etc.
|
||||
#
|
||||
# @par Basic usage
|
||||
# For example if a file defines confs like :
|
||||
# <pre>
|
||||
# [super_section]
|
||||
# super_conf = super_value
|
||||
# </pre>
|
||||
# You can access it with :
|
||||
# <pre> settings_instance.confs.super_section.super_conf </pre>
|
||||
#
|
||||
# @par Init sequence
|
||||
# The initialization sequence is a bit tricky. In fact, plugins adds allowed configuration
|
||||
# sections/values, but the list of plugins to load in in... the settings.
|
||||
# Here is the conceptual presentation of Settings class initialization stages :
|
||||
# -# Preloading (sets values like lodel2 library path or the plugins path)
|
||||
# -# Ask a @ref lodel.settings.setting_loader.SettingsLoader to load all configurations files
|
||||
# -# Fetch the list of plugins in the loaded settings
|
||||
# -# Merge plugins settings specification with the global lodel settings specs ( see @ref lodel.plugin )
|
||||
# -# Fetch all settings from the merged settings specs
|
||||
#
|
||||
# @par Init sequence in practical
|
||||
# In practice those steps are done by calling a succession of private methods :
|
||||
# -# @ref Settings.__bootstrap() ( steps 1 to 3 )
|
||||
# -# @ref Settings.__merge_specs() ( step 4 )
|
||||
# -# @ref Settings.__populate_from_specs() (step 5)
|
||||
# -# And finally @ref Settings.__confs_to_namedtuple()
|
||||
#
|
||||
# @todo handles default sections for variable sections (sections ending with '.*')
|
||||
class Settings(object):
|
||||
|
||||
|
|
@ -52,10 +78,12 @@ class Settings(object):
|
|||
self.__confs = dict()
|
||||
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
|
||||
# now we should have the self.__confs['lodel2']['plugins_paths']
|
||||
# and self.__confs['lodel2']['lib_path'] set
|
||||
self.__bootstrap()
|
||||
|
||||
## @brief Configuration keys accessor
|
||||
# @return All confs organised into named tuples
|
||||
@property
|
||||
def confs(self):
|
||||
return copy.copy(self.__confs)
|
||||
|
|
@ -112,9 +140,12 @@ class Settings(object):
|
|||
# Construct final specs dict replacing variable sections
|
||||
# by the actual existing sections
|
||||
variable_sections = [ section for section in specs if section.endswith('.*') ]
|
||||
print("DEBUG VARIABLE SECTIONS : ")
|
||||
for vsec in variable_sections:
|
||||
preffix = vsec[:-2]
|
||||
print("PREFFIX = ", preffix)
|
||||
for section in loader.getsection(preffix, 'default'): #WARNING : hardcoded default section
|
||||
print("SECTIONs = ", section)
|
||||
specs[section] = copy.copy(specs[vsec])
|
||||
del(specs[vsec])
|
||||
# Fetching valuds for sections
|
||||
|
|
|
|||
2
plugins/dummy/__init__.py
Normal file
2
plugins/dummy/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
__author__ = "Lodel2 dev team"
|
||||
__fullname__ = "Dummy plugin"
|
||||
17
plugins/dummy/main.py
Normal file
17
plugins/dummy/main.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
from lodel.plugin import LodelHook
|
||||
|
||||
## @brief Hook's callback example
|
||||
@LodelHook('leapi_get_pre')
|
||||
@LodelHook('leapi_get_post')
|
||||
@LodelHook('leapi_update_pre')
|
||||
@LodelHook('leapi_update_post')
|
||||
@LodelHook('leapi_delete_pre')
|
||||
@LodelHook('leapi_delete_post')
|
||||
@LodelHook('leapi_insert_pre')
|
||||
@LodelHook('leapi_insert_post')
|
||||
def dummy_callback(hook_name, caller, payload):
|
||||
if Lodel.settings.Settings.debug:
|
||||
print("\tHook %s\tcaller %s with %s" % (hook_name, caller, payload))
|
||||
return payload
|
||||
0
tests/settings/__init__.py
Normal file
0
tests/settings/__init__.py
Normal file
3
tests/settings/settings_tests.ini
Normal file
3
tests/settings/settings_tests.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
|
||||
4
tests/settings/settings_tests_conf.d/logger.ini
Normal file
4
tests/settings/settings_tests_conf.d/logger.ini
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[lodel2.logging.default]
|
||||
level = DEBUG
|
||||
context = True
|
||||
filename = -
|
||||
3
tests/settings/settings_tests_conf.d/settings.ini
Normal file
3
tests/settings/settings_tests_conf.d/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
|
||||
|
|
@ -1,42 +1,13 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
import unittest
|
||||
from lodel.settings.settings_loader import SettingsLoader
|
||||
from unittest import mock
|
||||
|
||||
class SettingsLoaderTestCase(unittest.TestCase):
|
||||
from lodel.settings.settings import Settings
|
||||
|
||||
def test_merge_getsection(self):
|
||||
"""Tests merge and getSection functions """
|
||||
settings = SettingsLoader('/home/helene/lodel2/tests/settings/conf.d')
|
||||
a = settings.getsection('A')
|
||||
self.assertEqual(a,dict({"a":"a1","b":"b1,b2,b3","c":"toto","fhui":"njl"}))
|
||||
b = settings.getsection('B')
|
||||
self.assertEqual(b,dict({"ab":"art","bb":"bj,kl,mn","cb":"tatat"}))
|
||||
c = settings.getsection('C')
|
||||
self.assertEqual(c,dict({"ca":"a2","cb":"b4,b2,b3","cc":"titi"}))
|
||||
d = settings.getsection('D')
|
||||
|
||||
for v in a:
|
||||
assert ('A','{"a":"a1","b":"b1,b2,b3","c":"toto","fhui":"njl"}')
|
||||
def maFonction(a):
|
||||
return a
|
||||
e=settings.getoption('A','a',maFonction)
|
||||
self.assertEqual(e,'a1')
|
||||
f=settings.getoption('B','bb',maFonction)
|
||||
self.assertEqual(f,"bj,kl,mn")
|
||||
g=settings.getremains()
|
||||
self.assertIsNotNone(g)
|
||||
e=settings.getoption('A','b',maFonction)
|
||||
e=settings.getoption('A','c',maFonction)
|
||||
e=settings.getoption('A','fhui',maFonction)
|
||||
f=settings.getoption('B','ab',maFonction)
|
||||
f=settings.getoption('B','cb',maFonction)
|
||||
f=settings.getoption('C','cb',maFonction)
|
||||
f=settings.getoption('C','ca',maFonction)
|
||||
f=settings.getoption('C','cc',maFonction)
|
||||
|
||||
g=settings.getremains()
|
||||
self.assertEqual(g,[])
|
||||
|
||||
|
||||
|
||||
class SettingsTestCase(unittest.TestCase):
|
||||
|
||||
def test_init(self):
|
||||
settings = Settings('tests/settings/settings_tests.ini', 'tests/settings/settings_tests_conf.d')
|
||||
print(settings.confs)
|
||||
pass
|
||||
|
|
|
|||
42
tests/settings/test_settings_loader.py
Normal file
42
tests/settings/test_settings_loader.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
import unittest
|
||||
from lodel.settings.settings_loader import SettingsLoader
|
||||
|
||||
class SettingsLoaderTestCase(unittest.TestCase):
|
||||
|
||||
def test_merge_getsection(self):
|
||||
"""Tests merge and getSection functions """
|
||||
settings = SettingsLoader('/home/helene/lodel2/tests/settings/conf.d')
|
||||
a = settings.getsection('A')
|
||||
self.assertEqual(a,dict({"a":"a1","b":"b1,b2,b3","c":"toto","fhui":"njl"}))
|
||||
b = settings.getsection('B')
|
||||
self.assertEqual(b,dict({"ab":"art","bb":"bj,kl,mn","cb":"tatat"}))
|
||||
c = settings.getsection('C')
|
||||
self.assertEqual(c,dict({"ca":"a2","cb":"b4,b2,b3","cc":"titi"}))
|
||||
d = settings.getsection('D')
|
||||
|
||||
for v in a:
|
||||
assert ('A','{"a":"a1","b":"b1,b2,b3","c":"toto","fhui":"njl"}')
|
||||
def maFonction(a):
|
||||
return a
|
||||
e=settings.getoption('A','a',maFonction)
|
||||
self.assertEqual(e,'a1')
|
||||
f=settings.getoption('B','bb',maFonction)
|
||||
self.assertEqual(f,"bj,kl,mn")
|
||||
g=settings.getremains()
|
||||
self.assertIsNotNone(g)
|
||||
e=settings.getoption('A','b',maFonction)
|
||||
e=settings.getoption('A','c',maFonction)
|
||||
e=settings.getoption('A','fhui',maFonction)
|
||||
f=settings.getoption('B','ab',maFonction)
|
||||
f=settings.getoption('B','cb',maFonction)
|
||||
f=settings.getoption('C','cb',maFonction)
|
||||
f=settings.getoption('C','ca',maFonction)
|
||||
f=settings.getoption('C','cc',maFonction)
|
||||
|
||||
g=settings.getremains()
|
||||
self.assertEqual(g,[])
|
||||
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue