From cddf221988ed9f1f1b2b3a2eb76cbbeb114edcf2 Mon Sep 17 00:00:00 2001 From: Yann Date: Thu, 18 Aug 2016 14:49:31 +0200 Subject: [PATCH] Lodel2 script helper class implementation (#122) - Allow to extend easily lodel_admin.py script. - Writtent first lodel_admin.py action : discover-plugins --- install/loader.py | 3 +- install/lodel_admin.py | 30 +++++--- lodel/plugin/core_scripts.py | 29 +++++++ lodel/plugin/exceptions.py | 3 + lodel/plugin/plugins.py | 10 ++- lodel/plugin/scripts.py | 145 +++++++++++++++++++++++++++++++++++ 6 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 lodel/plugin/core_scripts.py create mode 100644 lodel/plugin/scripts.py diff --git a/install/loader.py b/install/loader.py index 00a67f3..fd252f8 100644 --- a/install/loader.py +++ b/install/loader.py @@ -32,10 +32,11 @@ if 'LODEL2_NO_SETTINGS_LOAD' not in os.environ: if not settings.started(): settings('conf.d') from lodel.settings import Settings - + #Starts hooks from lodel.plugin import LodelHook from lodel.plugin import core_hooks + from lodel.plugin import core_scripts def start(): #Load plugins diff --git a/install/lodel_admin.py b/install/lodel_admin.py index 116bcf8..b799efb 100644 --- a/install/lodel_admin.py +++ b/install/lodel_admin.py @@ -4,14 +4,20 @@ import sys import os, os.path import argparse -""" -#Dirty hack to solve symlinks problems : -# When loader was imported the original one (LODEL_LIBDIR/install/loader) -# because lodel_admin.py is a symlink from this folder -#Another solution can be delete loader from install folder -sys.path[0] = os.getcwd() -import loader -""" +##@brief Dirty hack to avoid problems with simlink to lodel2 lib folder +# +#In instance folder we got a loader.py (the one we want to import here when +#writing "import loader". The problem is that lodel_admin.py is a simlink to +#LODEL2LIB_FOLDER/install/lodel_admin.py . In this folder there is the +#generic loader.py template. And when writing "import loader" its +#LODEL2LIB_FOLDER/install/loader.py that gets imported. +# +#In order to solve this problem the _simlink_hack() function delete the +#LODEL2LIB_FOLDER/install entry from sys.path +#@note This problem will be solved once lodel lib dir will be in +#/usr/lib/python3/lodel +def _simlink_hack(): + sys.path[0] = os.getcwd() ## @brief Utility method to generate python code given an emfile and a # translator @@ -141,4 +147,10 @@ def update_plugin_discover_cache(path_list = None): for pname, pinfos in res['plugins'].items(): print("\t- %s %s -> %s" % ( pname, pinfos['version'], pinfos['path'])) - + +if __name__ == '__main__': + _simlink_hack() + import loader + loader.start() + from lodel.plugin.scripts import main_run + main_run() diff --git a/lodel/plugin/core_scripts.py b/lodel/plugin/core_scripts.py new file mode 100644 index 0000000..be785f0 --- /dev/null +++ b/lodel/plugin/core_scripts.py @@ -0,0 +1,29 @@ +import lodel.plugin.scripts as lodel_script + +class DiscoverPlugin(lodel_script.LodelScript): + _action = 'discover-plugin' + _description = 'Walk through given folders looking for plugins' + + @classmethod + def argparser_config(cls, parser): + parser.add_argument('-d', '--directory', + help="Directory to walk through looking for lodel2 plugins", + nargs='+') + parser.add_argument('-l', '--list-only', default=False, + action = 'store_true', + help="Use this option to print a list of discovered plugins \ +without modifying existing cache") + + @classmethod + def run(cls, args): + from lodel.plugin.plugins import Plugin + if args.directory is None or len(args.directory) == 0: + cls.help_exit("Specify a least one directory") + no_cache = args.list_only + res = Plugin.discover(args.directory, no_cache) + print("Found plugins in : %s" % ', '.join(args.directory)) + for pname, pinfos in res['plugins'].items(): + print("\t- %s(%s) in %s" % ( + pname, pinfos['version'], pinfos['path'])) + + diff --git a/lodel/plugin/exceptions.py b/lodel/plugin/exceptions.py index ced7316..b72000c 100644 --- a/lodel/plugin/exceptions.py +++ b/lodel/plugin/exceptions.py @@ -1,2 +1,5 @@ class PluginError(Exception): pass + +class LodelScriptError(Exception): + pass diff --git a/lodel/plugin/plugins.py b/lodel/plugin/plugins.py index 698fe14..298dcb4 100644 --- a/lodel/plugin/plugins.py +++ b/lodel/plugin/plugins.py @@ -512,9 +512,12 @@ name differ from the one found in plugin's init file" ##@brief Reccursively walk throught paths to find plugin, then stores #found plugin in a file... + #@param paths list : list of directory paths + #@param no_cache bool : if true only return a list of found plugins + #without modifying the cache file #@return a dict {'path_list': [...], 'plugins': { see @ref _discover }} @classmethod - def discover(cls, paths = None): + def discover(cls, paths = None, no_cache = False): logger.info("Running plugin discover") if paths is None: paths = DEFAULT_PLUGINS_PATH_LIST @@ -535,8 +538,9 @@ name differ from the one found in plugin's init file" pass result = {'path_list': paths, 'plugins': result} #Writing to cache - with open(DISCOVER_CACHE_FILENAME, 'w+') as pdcache: - pdcache.write(json.dumps(result)) + if not no_cache: + with open(DISCOVER_CACHE_FILENAME, 'w+') as pdcache: + pdcache.write(json.dumps(result)) return result ##@brief Return discover result diff --git a/lodel/plugin/scripts.py b/lodel/plugin/scripts.py new file mode 100644 index 0000000..8cfa228 --- /dev/null +++ b/lodel/plugin/scripts.py @@ -0,0 +1,145 @@ +import argparse +import sys + +from lodel import logger +from lodel.exceptions import * + +##@brief Stores registered scripts +__registered_scripts = dict() + +##@brief LodelScript metaclass that allows to "catch" child class +#declaration +# +#Automatic script registration on child class declaration +class MetaLodelScript(type): + + def __init__(self, name, bases, attrs): + #Here we can store all child classes of LodelScript + super().__init__(name, bases, attrs) + if len(bases) == 1 and bases[0] == object: + print("Dropped : ", name, bases) + return + + self.__register_script(name) + #_action initialization + if self._action is None: + logger.warning("%s._action is None. Trying to use class name as \ +action identifier" % name) + self._action = name + self._action = self._action.lower() + if self._description is None: + self._description = self._default_description() + self._parser = argparse.ArgumentParser( + prog = self._prog_name, + description = self._description) + self.argparser_config(self._parser) + + + ##@brief Handles script registration + #@note Script list is maitained in + #lodel.plugin.admin_script.__registered_scripts + def __register_script(self, name): + if self._action is None: + logger.warning("%s._action is None. Trying to use class name as \ +action identifier" % name) + self._action = name + self._action = self._action.lower() + script_registration(self._action, self) + +class LodelScript(object, metaclass=MetaLodelScript): + + ##@brief A string to identify the action + _action = None + ##@brief Script descripiton (argparse argument) + _description = None + ##@brief argparse.ArgumentParser instance + _parser = None + + ##@brief No instanciation + def __init__(self): + raise NotImplementedError("Static class") + + ##@brief Virtual method. Designed to initialize arguement parser. + #@param argparser ArgumentParser : Child class argument parser instance + #@return MUST return the argument parser (NOT SURE ABOUT THAT !! Maybe it \ + #works by reference) + @classmethod + def argparser_config(cls, parser): + raise LodelScriptError("LodelScript.argparser_config() is a pure \ +virtual method! MUST be implemented by ALL child classes") + + ##@brief Virtual method. Run the script + #@return None or an integer that will be the script return code + @classmethod + def run(cls, args): + raise LodelScriptError("LodelScript.run() is a pure virtual method. \ +MUST be implemented by ALL child classes") + + ##@brief Called by main_run() to execute a script. + # + #Handles argument parsing and then call LodelScript.run() + @classmethod + def _run(cls): + args = cls._parser.parse_args() + return cls.run(args) + + ##@brief Append action name to the prog name + #@note See argparse.ArgumentParser() prog argument + @classmethod + def _prog_name(cls): + return '%s %s' % (sys.argv[0], self._action) + + ##@brief Return the default description for an action + @classmethod + def _default_description(cls): + return "Lodel2 script : %s" % cls._action + + @classmethod + def help_exit(cls,msg = None, return_code = 1, exit_after = True): + if not (msg is None): + print(msg, file=sys.stderr) + cls._parser.print_help() + if exit_after: + exit(1) + +def script_registration(action_name, cls): + __registered_scripts[action_name] = cls + logger.info("New script registered : %s" % action_name) + +##@brief Return a list containing all available actions +def _available_actions(): + return [ act for act in __registered_scripts ] + +##@brief Returns default runner argument parser +def _default_parser(): + + action_list = _available_actions() + if len(action_list) > 0: + action_list = ', '.join(sorted(action_list)) + else: + action_list = 'NO SCRIPT FOUND !' + + parser = argparse.ArgumentParser(description = "Lodel2 script runner") + parser.add_argument('action', metavar="ACTION", type=str, + help="One of the following actions : %s" % action_list) + parser.add_argument('option', metavar="OPTIONS", type=str, nargs='*', + help="Action options. Use %s ACTION -h to have help on a specific \ +action" % sys.argv[0]) + return parser + +def main_run(): + default_parser = _default_parser() + if len(sys.argv) == 1: + default_parser.print_help() + exit(1) + #preparing sys.argv (deleting action) + action = sys.argv[1].lower() + del(sys.argv[1]) + if action not in __registered_scripts: + print("Unknow action '%s'\n" % action, file=sys.stderr) + default_parser.print_help() + exit(1) + script = __registered_scripts[action] + ret = script._run() + ret = 0 if ret is None else ret + exit(ret)