Aucune description
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

plugins.py 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  1. #-*- coding: utf-8 -*-
  2. import sys
  3. import os.path
  4. import importlib
  5. import copy
  6. import json
  7. from importlib.machinery import SourceFileLoader, SourcelessFileLoader
  8. import plugins
  9. from .exceptions import *
  10. from lodel import logger
  11. ## @package lodel.plugins Lodel2 plugins management
  12. #
  13. # Lodel2 plugins are stored in directories
  14. # A typicall lodel2 plugin directory structure looks like :
  15. # - {{__init__.py}}} containing informations like full_name, authors, licence etc.
  16. # - main.py containing hooks registration etc
  17. # - confspec.py containing a configuration specification dictionary named CONFSPEC
  18. ##@brief The package in which we will load plugins modules
  19. VIRTUAL_PACKAGE_NAME = 'lodel.plugins'
  20. ##@brief The temporary package to import python sources
  21. VIRTUAL_TEMP_PACKAGE_NAME = 'lodel.plugin_tmp'
  22. ##@brief Plugin init filename
  23. INIT_FILENAME = '__init__.py' # Loaded with settings
  24. PLUGIN_NAME_VARNAME = '__plugin_name__'
  25. PLUGIN_TYPE_VARNAME = '__type__'
  26. PLUGIN_VERSION_VARNAME = '__version__'
  27. CONFSPEC_FILENAME_VARNAME = '__confspec__'
  28. CONFSPEC_VARNAME = 'CONFSPEC'
  29. LOADER_FILENAME_VARNAME = '__loader__'
  30. PLUGIN_DEPS_VARNAME = '__plugin_deps__'
  31. ACTIVATE_METHOD_NAME = '_activate'
  32. ##@brief Discover stage cache filename
  33. DISCOVER_CACHE_FILENAME = '.plugin_discover_cache.json'
  34. ##@brief Default & failover value for plugins path list
  35. DEFAULT_PLUGINS_PATH_LIST = ['./plugins']
  36. MANDATORY_VARNAMES = [PLUGIN_NAME_VARNAME, LOADER_FILENAME_VARNAME,
  37. PLUGIN_VERSION_VARNAME]
  38. EXTENSIONS = 'default'
  39. PLUGINS_TYPES = [EXTENSIONS, 'datasource', 'session_handler', 'ui']
  40. ##@brief Describe and handle version numbers
  41. class PluginVersion(object):
  42. PROPERTY_LIST = ['major', 'minor', 'revision' ]
  43. ##@brief Version constructor
  44. #@param *args : You can either give a str that will be splitted on . or you
  45. #can give a iterable containing 3 integer or 3 arguments representing
  46. #major, minor and revision version
  47. def __init__(self, *args):
  48. self.__version = [0 for _ in range(3) ]
  49. if len(args) == 1:
  50. arg = args[0]
  51. if isinstance(arg, str):
  52. spl = arg.split('.')
  53. invalid = False
  54. if len(spl) > 3:
  55. raise PluginError("The string '%s' is not a valid plugin \
  56. version number" % arg)
  57. else:
  58. try:
  59. if len(arg) >= 1:
  60. if len(arg) > 3:
  61. raise PluginError("Expected maximum 3 value to \
  62. create a plugin version number but found '%s' as argument" % arg)
  63. for i, v in enumerate(arg):
  64. self.__version[i] = arg[i]
  65. except TypeError:
  66. raise PluginError("Unable to convert argument into plugin \
  67. version number" % arg)
  68. elif len(args) > 3:
  69. raise PluginError("Expected between 1 and 3 positional arguments \
  70. but %d arguments found" % len(args))
  71. else:
  72. for i,v in enumerate(args):
  73. self.__version[i] = v
  74. @property
  75. def major(self):
  76. return self.__version[0]
  77. @property
  78. def minor(self):
  79. return self.__version[1]
  80. @property
  81. def revision(self):
  82. return self.__version[2]
  83. ##@brief Check and prepare comparisoon argument
  84. #@return A PluginVersion instance
  85. #@throw PluginError if invalid argument provided
  86. def __cmp_check(self, other):
  87. if not isinstance(other, PluginVersion):
  88. try:
  89. if len(other) <= 3 and len(other) > 0:
  90. return PluginVersion(other)
  91. except TypeError:
  92. raise PluginError("Cannot compare argument '%s' with \
  93. a PluginVerison instance" % other)
  94. return other
  95. ##@brief Generic comparison function
  96. #@param other PluginVersion or iterable
  97. #@param cmp_fun_name function : interger comparison function
  98. def __generic_cmp(self, other, cmp_fun_name):
  99. other = self.__cmp_check(other)
  100. try:
  101. cmpfun = getattr(int, cmp_fun_name)
  102. except AttributeError:
  103. raise LodelFatalError("Invalid comparison callback given \
  104. to generic PluginVersion comparison function : '%s'" % cmp_fun_name)
  105. for property_name in self.PROPERTY_LIST:
  106. if not cmpfun(getattr(self, pname), getattr(other, pname)):
  107. return False
  108. return True
  109. def __lt__(self, other):
  110. return self.__generic_cmp(other, '__lt__')
  111. def __le__(self, other):
  112. return self.__generic_cmp(other, '__le__')
  113. def __eq__(self, other):
  114. return self.__generic_cmp(other, '__eq__')
  115. def __ne__(self, other):
  116. return self.__generic_cmp(other, '__ne__')
  117. def __gt__(self, other):
  118. return self.__generic_cmp(other, '__gt__')
  119. def __ge__(self, other):
  120. return self.__generic_cmp(other, '__ge__')
  121. def __str__(self):
  122. return '%d.%d.%d' % tuple(self.__version)
  123. def __repr__(self):
  124. return {'major': self.major, 'minor': self.minor,
  125. 'revision': self.revision}
  126. ##@brief Handle plugins
  127. #
  128. # An instance represent a loaded plugin. Class methods allow to load/preload
  129. # plugins.
  130. #
  131. # Typicall Plugins load sequence is :
  132. # 1. Settings call start method to instanciate all plugins found in confs
  133. # 2. Settings fetch all confspecs
  134. # 3. the loader call load_all to register hooks etc
  135. class Plugin(object):
  136. ##@brief Stores plugin directories paths
  137. _plugin_directories = None
  138. ##@brief Stores Plugin instances indexed by name
  139. _plugin_instances = dict()
  140. ##@brief Attribute used by load_all and load methods to detect circular
  141. #dependencies
  142. _load_called = []
  143. ##@brief Attribute that stores plugins list from discover cache file
  144. _plugin_list = None
  145. ##@brief Plugin class constructor
  146. #
  147. # Called by setting in early stage of lodel2 boot sequence using classmethod
  148. # register
  149. #
  150. # @param plugin_name str : plugin name
  151. # @throw PluginError
  152. def __init__(self, plugin_name):
  153. self.started()
  154. self.name = plugin_name
  155. self.path = self.plugin_path(plugin_name)
  156. ##@brief Stores the plugin module
  157. self.module = None
  158. ##@breif Stores the plugin loader module
  159. self.__loader_module = None
  160. self.__confspecs = dict()
  161. self.loaded = False
  162. # Importing __init__.py infos in it
  163. plugin_module = '%s.%s' % (VIRTUAL_PACKAGE_NAME,
  164. plugin_name)
  165. init_source = os.path.join(self.path, INIT_FILENAME)
  166. try:
  167. loader = SourceFileLoader(plugin_module, init_source)
  168. self.module = loader.load_module()
  169. except (ImportError,FileNotFoundError) as e:
  170. raise PluginError("Failed to load plugin '%s'. It seems that the plugin name is not valid or the plugin do not exists" % plugin_name)
  171. # loading confspecs
  172. try:
  173. # Loading confspec directly from __init__.py
  174. self.__confspecs = getattr(self.module, CONFSPEC_VARNAME)
  175. except AttributeError:
  176. # Loading file in __confspec__ var in __init__.py
  177. try:
  178. module = self._import_from_init_var(CONFSPEC_FILENAME_VARNAME)
  179. except AttributeError:
  180. msg = "Malformed plugin {plugin} . No {varname} not {filevar} found in __init__.py"
  181. msg = msg.format(
  182. plugin = self.name,
  183. varname = CONFSPEC_VARNAME,
  184. filevar = CONFSPEC_FILENAME_VARNAME)
  185. raise PluginError(msg)
  186. except ImportError as e:
  187. msg = "Broken plugin {plugin} : {expt}"
  188. msg = msg.format(
  189. plugin = self.name,
  190. expt = str(e))
  191. raise PluginError(msg)
  192. try:
  193. # loading confpsecs from file
  194. self.__confspecs = getattr(module, CONFSPEC_VARNAME)
  195. except AttributeError:
  196. msg = "Broken plugin. {varname} not found in '{filename}'"
  197. msg = msg.format(
  198. varname = CONFSPEC_VARNAME,
  199. filename = confspec_filename)
  200. raise PluginError(msg)
  201. # loading plugin version
  202. try:
  203. #this try block should be useless. The existance of
  204. #PLUGIN_VERSION_VARNAME in init file is mandatory
  205. self.__version = getattr(self.module, PLUGIN_VERSION_VARNAME)
  206. except AttributeError:
  207. msg = "Error that should not append : no %s found in plugin \
  208. init file. Malformed plugin"
  209. msg %= PLUGIN_VERSION_VARNAME
  210. raise LodelFatalError(msg)
  211. # Load plugin type
  212. try:
  213. self.__type = getattr(self.module, PLUGIN_TYPE_VARNAME)
  214. except AttributeError:
  215. self.__type = EXTENSIONS
  216. self.__type = str(self.__type).lower()
  217. if self.__type not in PLUGINS_TYPES:
  218. raise PluginError("Unknown plugin type '%s'" % self.__type)
  219. # Load plugin name from init file (just for checking)
  220. try:
  221. #this try block should be useless. The existance of
  222. #PLUGIN_NAME_VARNAME in init file is mandatory
  223. pname = getattr(self.module, PLUGIN_NAME_VARNAME)
  224. except AttributeError:
  225. msg = "Error that should not append : no %s found in plugin \
  226. init file. Malformed plugin"
  227. msg %= PLUGIN_NAME_VARNAME
  228. raise LodelFatalError(msg)
  229. if pname != plugin_name:
  230. msg = "Plugin's discover cache inconsistency detected ! Cached \
  231. name differ from the one found in plugin's init file"
  232. raise PluginError(msg)
  233. ##@brief Try to import a file from a variable in __init__.py
  234. #@param varname str : The variable name
  235. #@return loaded module
  236. #@throw AttributeError if varname not found
  237. #@throw ImportError if the file fails to be imported
  238. #@throw PluginError if the filename was not valid
  239. def _import_from_init_var(self, varname):
  240. # Read varname
  241. try:
  242. filename = getattr(self.module, varname)
  243. except AttributeError:
  244. msg = "Malformed plugin {plugin}. No {varname} found in __init__.py"
  245. msg = msg.format(
  246. plugin = self.name,
  247. varname = LOADER_FILENAME_VARNAME)
  248. raise PluginError(msg)
  249. #Path are not allowed
  250. if filename != os.path.basename(filename):
  251. msg = "Invalid {varname} content : '{fname}' for plugin {name}"
  252. msg = msg.format(
  253. varname = varname,
  254. fname = filename,
  255. name = self.name)
  256. raise PluginError(msg)
  257. # importing the file in varname
  258. module_name = self.module.__name__+"."+varname
  259. filename = os.path.join(self.path, filename)
  260. loader = SourceFileLoader(module_name, filename)
  261. return loader.load_module()
  262. ##@brief Check dependencies of plugin
  263. #@return A list of plugin name to be loaded before
  264. def check_deps(self):
  265. try:
  266. res = getattr(self.module, PLUGIN_DEPS_VARNAME)
  267. except AttributeError:
  268. return list()
  269. result = list()
  270. errors = list()
  271. for plugin_name in res:
  272. try:
  273. result.append(self.get(plugin_name))
  274. except PluginError:
  275. errors.append(plugin_name)
  276. if len(errors) > 0:
  277. raise PluginError( "Bad dependencie for '%s' :"%self.name,
  278. ', '.join(errors))
  279. return result
  280. ##@brief Check if the plugin should be activated
  281. #
  282. #Try to fetch a function called @ref ACTIVATE_METHOD_NAME in __init__.py
  283. #of a plugin. If none found assert that the plugin can be loaded, else
  284. #the method is called. If it returns anything else that True, the plugin
  285. #is noted as not activable
  286. #
  287. # @note Maybe we have to exit everything if a plugin cannot be loaded...
  288. def activable(self):
  289. from lodel import logger
  290. try:
  291. test_fun = getattr(self.module, ACTIVATE_METHOD_NAME)
  292. except AttributeError:
  293. msg = "No %s method found for plugin %s. Assuming plugin is ready to be loaded"
  294. msg %= (ACTIVATE_METHOD_NAME, self.name)
  295. logger.debug(msg)
  296. test_fun = lambda:True
  297. return test_fun()
  298. ##@brief Load a plugin
  299. #
  300. #Loading a plugin means importing a file. The filename is defined in the
  301. #plugin's __init__.py file in a LOADER_FILENAME_VARNAME variable.
  302. #
  303. #The loading process has to take care of other things :
  304. #- loading dependencies (other plugins)
  305. #- check that the plugin can be activated using Plugin.activate() method
  306. #- avoid circular dependencies infinite loop
  307. def _load(self):
  308. if self.loaded:
  309. return
  310. from lodel import logger
  311. #Test that plugin "wants" to be activated
  312. activable = self.activable()
  313. if not(activable is True):
  314. msg = "Plugin %s is not activable : %s"
  315. msg %= (self.name, activable)
  316. raise PluginError(activable)
  317. #Circular dependencie detection
  318. if self.name in self._load_called:
  319. raise PluginError("Circular dependencie in Plugin detected. Abording")
  320. else:
  321. self._load_called.append(self.name)
  322. #Dependencie load
  323. for dependencie in self.check_deps():
  324. activable = dependencie.activable()
  325. if activable is True:
  326. dependencie._load()
  327. else:
  328. msg = "Plugin {plugin_name} not activable because it depends on plugin {dep_name} that is not activable : {reason}"
  329. msg = msg.format(
  330. plugin_name = self.name,
  331. dep_name = dependencie.name,
  332. reason = activable)
  333. #Loading the plugin
  334. try:
  335. self.__loader_module = self._import_from_init_var(LOADER_FILENAME_VARNAME)
  336. except PluginError as e:
  337. raise e
  338. except ImportError as e:
  339. msg = "Broken plugin {plugin} : {expt}"
  340. msg = msg.format(
  341. plugin = self.name,
  342. expt = str(e))
  343. raise PluginError(msg)
  344. logger.debug("Plugin '%s' loaded" % self.name)
  345. self.loaded = True
  346. def loader_module(self):
  347. if not self.loaded:
  348. raise RuntimeError("Plugin %s not loaded yet."%self.name)
  349. return self.__loader_module
  350. def __str__(self):
  351. return "<LodelPlugin '%s' version %s>" % (self.name, self.__version)
  352. ##@brief Call load method on every pre-loaded plugins
  353. #
  354. # Called by loader to trigger hooks registration.
  355. # This method have to avoid circular dependencies infinite loops. For this
  356. # purpose a class attribute _load_called exists.
  357. # @throw PluginError
  358. @classmethod
  359. def load_all(cls):
  360. errors = dict()
  361. cls._load_called = []
  362. for name, plugin in cls._plugin_instances.items():
  363. try:
  364. plugin._load()
  365. except PluginError as e:
  366. errors[name] = e
  367. if len(errors) > 0:
  368. msg = "Errors while loading plugins :"
  369. for name, e in errors.items():
  370. msg += "\n\t%20s : %s" % (name,e)
  371. msg += "\n"
  372. raise PluginError(msg)
  373. from lodel.plugin.hooks import LodelHook
  374. LodelHook.call_hook(
  375. "lodel2_plugins_loaded", cls, cls._plugin_instances)
  376. ##@return a copy of __confspecs attr
  377. @property
  378. def confspecs(self):
  379. return copy.copy(self.__confspecs)
  380. ##@brief Register a new plugin
  381. #
  382. #@param plugin_name str : The plugin name
  383. #@return a Plugin instance
  384. #@throw PluginError
  385. @classmethod
  386. def register(cls, plugin_name):
  387. if plugin_name in cls._plugin_instances:
  388. msg = "Plugin allready registered with same name %s"
  389. msg %= plugin_name
  390. raise PluginError(msg)
  391. plugin = cls(plugin_name)
  392. cls._plugin_instances[plugin_name] = plugin
  393. logger.debug("Plugin %s available." % plugin)
  394. return plugin
  395. ##@brief Plugins instances accessor
  396. #
  397. #@param plugin_name str: The plugin name
  398. #@return a Plugin instance
  399. #@throw PluginError if plugin not found
  400. @classmethod
  401. def get(cls, plugin_name):
  402. try:
  403. return cls._plugin_instances[plugin_name]
  404. except KeyError:
  405. msg = "No plugin named '%s' loaded"
  406. msg %= plugin_name
  407. raise PluginError(msg)
  408. ##@brief Given a plugin name returns the plugin path
  409. # @param plugin_name str : The plugin name
  410. # @return the plugin directory path
  411. @classmethod
  412. def plugin_path(cls, plugin_name):
  413. cls.started()
  414. plist = cls.plugin_list()
  415. if plugin_name not in plist:
  416. raise PluginError("No plugin named '%s' found" % plugin_name)
  417. try:
  418. return cls.get(plugin_name).path
  419. except PluginError:
  420. pass
  421. return plist[plugin_name]['path']
  422. @classmethod
  423. def plugin_module_name(cls, plugin_name):
  424. return "%s.%s" % (VIRTUAL_PACKAGE_NAME, plugin_name)
  425. ##@brief Start the Plugin class
  426. #
  427. # Called by Settings.__bootstrap()
  428. #
  429. # This method load path and preload plugins
  430. @classmethod
  431. def start(cls, plugins_directories, plugins):
  432. if cls._plugin_directories is not None:
  433. return
  434. import inspect
  435. self_path = inspect.getsourcefile(Plugin)
  436. default_plugin_path = os.path.abspath(self_path + '../../../../plugins')
  437. if plugins_directories is None:
  438. plugins_directories = list()
  439. plugins_directories += [ default_plugin_path ]
  440. cls._plugin_directories = list(set(plugins_directories))
  441. for plugin_name in plugins:
  442. cls.register(plugin_name)
  443. @classmethod
  444. def started(cls, raise_if_not = True):
  445. res = cls._plugin_directories is not None
  446. if raise_if_not and not res:
  447. raise RuntimeError("Class Plugins is not initialized")
  448. @classmethod
  449. def clear(cls):
  450. if cls._plugin_directories is not None:
  451. cls._plugin_directories = None
  452. if cls._plugin_instances != dict():
  453. cls._plugin_instances = dict()
  454. if cls._load_called != []:
  455. cls._load_called = []
  456. ##@brief Reccursively walk throught paths to find plugin, then stores
  457. #found plugin in a file...
  458. #@param paths list : list of directory paths
  459. #@param no_cache bool : if true only return a list of found plugins
  460. #without modifying the cache file
  461. #@return a dict {'path_list': [...], 'plugins': { see @ref _discover }}
  462. #@todo add max_depth and symlink following options
  463. @classmethod
  464. def discover(cls, paths = None, no_cache = False):
  465. logger.info("Running plugin discover")
  466. if paths is None:
  467. paths = DEFAULT_PLUGINS_PATH_LIST
  468. tmp_res = []
  469. for path in paths:
  470. tmp_res += cls._discover(path)
  471. #Formating and dedoubloning result
  472. result = dict()
  473. for pinfos in tmp_res:
  474. pname = pinfos['name']
  475. if (
  476. pname in result
  477. and pinfos['version'] > result[pname]['version'])\
  478. or pname not in result:
  479. result[pname] = pinfos
  480. else:
  481. #dropped
  482. pass
  483. result = {'path_list': paths, 'plugins': result}
  484. #Writing to cache
  485. if not no_cache:
  486. with open(DISCOVER_CACHE_FILENAME, 'w+') as pdcache:
  487. pdcache.write(json.dumps(result))
  488. return result
  489. ##@brief Return discover result
  490. #@param refresh bool : if true invalidate all plugin list cache
  491. #@note If discover cache file not found run discover first
  492. #@note if refresh is set to True discover MUST have been run at least
  493. #one time. In fact refresh action load the list of path to explore
  494. #from the plugin's discover cache
  495. @classmethod
  496. def plugin_list(cls, refresh = False):
  497. try:
  498. infos = cls._load_discover_cache()
  499. path_list = infos['path_list']
  500. except PluginError:
  501. refresh = True
  502. path_list = DEFAULT_PLUGINS_PATH_LIST
  503. if cls._plugin_list is None or refresh:
  504. if not os.path.isfile(DISCOVER_CACHE_FILENAME) or refresh:
  505. infos = cls.discover(path_list)
  506. cls._plugin_list = infos['plugins']
  507. return cls._plugin_list
  508. ##@brief Attempt to open and load plugin discover cache
  509. #@return discover cache
  510. #@throw PluginError when open or load fails
  511. @classmethod
  512. def _load_discover_cache(cls):
  513. try:
  514. pdcache = open(DISCOVER_CACHE_FILENAME, 'r')
  515. except Exception as e:
  516. msg = "Unable to open discover cache : %s"
  517. msg %= e
  518. raise PluginError(msg)
  519. try:
  520. res = json.load(pdcache)
  521. except Exception as e:
  522. msg = "Unable to load discover cache : %s"
  523. msg %= e
  524. raise PluginError(msg)
  525. pdcache.close()
  526. return res
  527. ##@brief Check if a directory is a plugin module
  528. #@param path str : path to check
  529. #@return a dict with name, version and path if path is a plugin module, else False
  530. @classmethod
  531. def dir_is_plugin(cls, path):
  532. log_msg = "%s is not a plugin directory because : " % path
  533. #Checks that path exists
  534. if not os.path.isdir(path):
  535. raise ValueError(
  536. "Expected path to be a directory, but '%s' found" % path)
  537. #Checks that path contains plugin's init file
  538. initfile = os.path.join(path, INIT_FILENAME)
  539. if not os.path.isfile(initfile):
  540. log_msg += "'%s' not found" % (INIT_FILENAME)
  541. logger.debug(log_msg)
  542. return False
  543. #Importing plugin's init file to check contained datas
  544. try:
  545. initmod, modname = cls.import_init(path)
  546. except PluginError as e:
  547. log_msg += "unable to load '%s'. Exception raised : %s"
  548. log_msg %= (INIT_FILENAME, e)
  549. logger.debug(log_msg)
  550. return False
  551. #Checking mandatory init module variables
  552. for attr_name in MANDATORY_VARNAMES:
  553. if not hasattr(initmod,attr_name):
  554. log_msg += " mandatory variable '%s' not found in '%s'"
  555. log_msg %= (attr_name, INIT_FILENAME)
  556. logger.debug(log_msg)
  557. return False
  558. try:
  559. pversion = getattr(initmod, PLUGIN_VERSION_VARNAME)
  560. except PluginError as e:
  561. msg = "Invalid plugin version found in %s : %s"
  562. msg %= (path, e)
  563. raise PluginError(msg)
  564. pname = getattr(initmod, PLUGIN_NAME_VARNAME)
  565. return {'name': pname,
  566. 'version': pversion,
  567. 'path': path}
  568. ##@brief Import init file from a plugin path
  569. #@param path str : Directory path
  570. #@return a tuple (init_module, module_name)
  571. @classmethod
  572. def import_init(self, path):
  573. init_source = os.path.join(path, INIT_FILENAME)
  574. temp_module = '%s.%s.%s' % (
  575. VIRTUAL_TEMP_PACKAGE_NAME, os.path.basename(os.path.dirname(path)),
  576. 'test_init')
  577. try:
  578. loader = SourceFileLoader(temp_module, init_source)
  579. except (ImportError, FileNotFoundError) as e:
  580. raise PluginError("Unable to import init file from '%s' : %s" % (
  581. temp_module, e))
  582. try:
  583. res_module = loader.load_module()
  584. except Exception as e:
  585. raise PluginError("Unable to import initfile")
  586. return (res_module, temp_module)
  587. ##@brief Reccursiv plugin discover given a path
  588. #@param path str : the path to walk through
  589. #@return A dict with plugin_name as key and {'path':..., 'version':...} as value
  590. @classmethod
  591. def _discover(cls, path):
  592. res = []
  593. to_explore = [path]
  594. while len(to_explore) > 0:
  595. cur_path = to_explore.pop()
  596. for f in os.listdir(cur_path):
  597. f_path = os.path.join(cur_path, f)
  598. if f not in ['.', '..'] and os.path.isdir(f_path):
  599. #Check if it is a plugin directory
  600. test_result = cls.dir_is_plugin(f_path)
  601. if not (test_result is False):
  602. logger.info("Plugin found in %s" % f_path)
  603. res.append(test_result)
  604. else:
  605. to_explore.append(f_path)
  606. return res
  607. ##@brief Decorator class designed to allow plugins to add custom methods
  608. #to LeObject childs (dyncode objects)
  609. #
  610. class CustomMethod(object):
  611. ##@brief Stores registered custom methods
  612. #
  613. #Key = LeObject child class name
  614. #Value = CustomMethod instance
  615. _custom_methods = dict()
  616. INSTANCE_METHOD = 0
  617. CLASS_METHOD = 1
  618. STATIC_METHOD = 2
  619. ##@brief Decorator constructor
  620. #@param component_name str : the name of the component to enhance
  621. #@param method_name str : the name of the method to inject (if None given
  622. #@param method_type int : take value in one of
  623. #CustomMethod::INSTANCE_METHOD CustomMethod::CLASS_METHOD or
  624. #CustomMethod::STATIC_METHOD
  625. #use the function name
  626. def __init__(self, component_name, method_name = None, method_type=0):
  627. ##@brief The targeted LeObject child class
  628. self._comp_name = component_name
  629. ##@brief The method name
  630. self._method_name = method_name
  631. ##@brief The function (that will be the injected method)
  632. self._fun = None
  633. ##@brief Stores the type of method (instance, class or static)
  634. self._type = int(method_type)
  635. if self._type not in (self.INSTANCE_METHOD, self.CLASS_METHOD,\
  636. self.STATIC_METHOD):
  637. raise ValueError("Excepted value for method_type was one of \
  638. CustomMethod::INSTANCE_METHOD CustomMethod::CLASS_METHOD or \
  639. CustomMethod::STATIC_METHOD, but got %s" % self._type)
  640. ##@brief called just after __init__
  641. #@param fun function : the decorated function
  642. #@param return the function
  643. def __call__(self, fun):
  644. if self._method_name is None:
  645. self._method_name = fun.__name__
  646. if self._comp_name not in self._custom_methods:
  647. self._custom_methods[self._comp_name] = list()
  648. if self._method_name in [ scm._method_name for scm in self._custom_methods[self._comp_name]]:
  649. raise RuntimeError("A method named %s allready registered by \
  650. another plugin : %s" % (
  651. self._method_name,
  652. self._custom_methods[self._comp_name].__module__))
  653. self._fun = fun
  654. self._custom_methods[self._comp_name].append(self)
  655. ##@brief Textual representation
  656. #@return textual representation of the CustomMethod instance
  657. def __repr__(self):
  658. res = "<CustomMethod name={method_name} target={classname} \
  659. source={module_name}.{fun_name}>"
  660. return res.format(
  661. method_name = self._method_name,
  662. classname = self._comp_name,
  663. module_name = self._fun.__module__,
  664. fun_name = self._fun.__name__)
  665. ##@brief Return a well formed method
  666. #
  667. #@note the type of method depends on the _type attribute
  668. #@return a method directly injectable in the target class
  669. def __get_method(self):
  670. if self._type == self.INSTANCE_METHOD:
  671. def custom__get__(self, obj, objtype = None):
  672. return types.MethodType(self, obj, objtype)
  673. setattr(self._fun, '__get__', custom__get__)
  674. return self._fun
  675. elif self._type == self.CLASS_METHOD:
  676. return classmethod(self._fun)
  677. elif self._type == self.STATIC_METHOD:
  678. return staticmethod(self._fun)
  679. else:
  680. raise RuntimeError("Attribute _type is not one of \
  681. CustomMethod::INSTANCE_METHOD CustomMethod::CLASS_METHOD \
  682. CustomMethod::STATIC_METHOD")
  683. ##@brief Handle custom method dynamic injection in LeAPI dyncode
  684. #
  685. #Called by lodel2_dyncode_loaded hook defined at
  686. #lodel.plugin.core_hooks.lodel2_plugin_custom_methods()
  687. #
  688. #@param cls
  689. #@param dynclasses LeObject child classes : List of dynamically generated
  690. #LeObject child classes
  691. @classmethod
  692. def set_registered(cls, dynclasses):
  693. from lodel import logger
  694. dyn_cls_dict = { dc.__name__:dc for dc in dynclasses}
  695. for cls_name, custom_methods in cls._custom_methods.items():
  696. for custom_method in custom_methods:
  697. if cls_name not in dyn_cls_dict:
  698. logger.error("Custom method %s adding fails : No dynamic \
  699. LeAPI objects named %s." % (custom_method, cls_name))
  700. elif custom_method._method_name in dir(dyn_cls_dict[cls_name]):
  701. logger.warning("Overriding existing method '%s' on target \
  702. with %s" % (custom_method._method_name, custom_method))
  703. else:
  704. setattr(
  705. dyn_cls_dict[cls_name],
  706. custom_method._method_name,
  707. custom_method.__get_method())
  708. logger.debug(
  709. "Custom method %s added to target" % custom_method)
  710. ##@page lodel2_plugins Lodel2 plugins system
  711. #
  712. # @par Plugin structure
  713. #A plugin is a package (a folder containing, at least, an __init__.py file.
  714. #This file should expose multiple things :
  715. # - a CONFSPEC variable containing configuration specifications
  716. # - an _activate() method that returns True if the plugin can be activated (
  717. # optionnal)
  718. #
  719. class SessionHandler(Plugin):
  720. __instance = None
  721. def __init__(self, plugin_name):
  722. if self.__instance is None:
  723. super(Plugin, self).__init__(plugin_name)
  724. self.__instance = True
  725. else:
  726. raise RuntimeError("A SessionHandler Plugin is already plug")
  727. class InterfacePlugin(Plugin):
  728. def __init__(self, plugin_name):
  729. super(Plugin, self).__init__(plugin_name)