Keine Beschreibung
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

plugins.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. #-*- coding: utf-8 -*-
  2. import sys
  3. import os.path
  4. import importlib
  5. import copy
  6. from importlib.machinery import SourceFileLoader, SourcelessFileLoader
  7. import plugins
  8. from .exceptions import *
  9. ## @package lodel.plugins Lodel2 plugins management
  10. #
  11. # Lodel2 plugins are stored in directories
  12. # A typicall lodel2 plugin directory structure looks like :
  13. # - {{__init__.py}}} containing informations like full_name, authors, licence etc.
  14. # - main.py containing hooks registration etc
  15. # - confspec.py containing a configuration specification dictionary named CONFSPEC
  16. ##@brief The package in which we will load plugins modules
  17. VIRTUAL_PACKAGE_NAME = 'lodel.plugins'
  18. INIT_FILENAME = '__init__.py' # Loaded with settings
  19. CONFSPEC_FILENAME_VARNAME = '__confspec__'
  20. CONFSPEC_VARNAME = 'CONFSPEC'
  21. LOADER_FILENAME_VARNAME = '__loader__'
  22. PLUGIN_DEPS_VARNAME = '__plugin_deps__'
  23. ACTIVATE_METHOD_NAME = '_activate'
  24. ##@brief Handle plugins
  25. #
  26. # An instance represent a loaded plugin. Class methods allow to load/preload
  27. # plugins.
  28. #
  29. # Typicall Plugins load sequence is :
  30. # 1. Settings call start method to instanciate all plugins found in confs
  31. # 2. Settings fetch all confspecs
  32. # 3. the loader call load_all to register hooks etc
  33. class Plugin(object):
  34. ##@brief Stores plugin directories paths
  35. _plugin_directories = None
  36. ##@brief Stores Plugin instances indexed by name
  37. _plugin_instances = dict()
  38. ##@brief Attribute used by load_all and load methods to detect circular
  39. #dependencies
  40. _load_called = []
  41. ##@brief Plugin class constructor
  42. #
  43. # Called by setting in early stage of lodel2 boot sequence using classmethod
  44. # register
  45. #
  46. # @param plugin_name str : plugin name
  47. # @throw PluginError
  48. def __init__(self, plugin_name):
  49. self.started()
  50. self.name = plugin_name
  51. self.path = self.plugin_path(plugin_name)
  52. ##@brief Stores the plugin module
  53. self.module = None
  54. ##@breif Stores the plugin loader module
  55. self.__loader_module = None
  56. self.__confspecs = dict()
  57. self.loaded = False
  58. # Importing __init__.py
  59. plugin_module = '%s.%s' % (VIRTUAL_PACKAGE_NAME,
  60. plugin_name)
  61. init_source = self.path + INIT_FILENAME
  62. try:
  63. loader = SourceFileLoader(plugin_module, init_source)
  64. self.module = loader.load_module()
  65. except (ImportError,FileNotFoundError) as e:
  66. raise PluginError("Failed to load plugin '%s'. It seems that the plugin name is not valid or the plugin do not exists" % plugin_name)
  67. # loading confspecs
  68. try:
  69. # Loading confspec directly from __init__.py
  70. self.__confspecs = getattr(self.module, CONFSPEC_VARNAME)
  71. except AttributeError:
  72. # Loading file in __confspec__ var in __init__.py
  73. try:
  74. module = self._import_from_init_var(CONFSPEC_FILENAME_VARNAME)
  75. except AttributeError:
  76. msg = "Malformed plugin {plugin} . No {varname} not {filevar} found in __init__.py"
  77. msg = msg.format(
  78. plugin = self.name,
  79. varname = CONFSPEC_VARNAME,
  80. filevar = CONFSPEC_FILENAME_VARNAME)
  81. raise PluginError(msg)
  82. except ImportError as e:
  83. msg = "Broken plugin {plugin} : {expt}"
  84. msg = msg.format(
  85. plugin = self.name,
  86. expt = str(e))
  87. raise PluginError(msg)
  88. try:
  89. # loading confpsecs from file
  90. self.__confspecs = getattr(module, CONFSPEC_VARNAME)
  91. except AttributeError:
  92. msg = "Broken plugin. {varname} not found in '{filename}'"
  93. msg = msg.format(
  94. varname = CONFSPEC_VARNAME,
  95. filename = confspec_filename)
  96. raise PluginError(msg)
  97. ##@brief Browse directory to get plugin
  98. #@param plugin_path
  99. #@return module existing
  100. def _discover_plugin(self, plugin_path):
  101. import os
  102. try:
  103. for root, dirs, files in os.walk(plugin_path, topdown = True):
  104. return dirs
  105. except NameError:
  106. msg = "This plugin {plugin_path} is not valid"
  107. ##@brief Try to import a file from a variable in __init__.py
  108. #@param varname str : The variable name
  109. #@return loaded module
  110. #@throw AttributeError if varname not found
  111. #@throw ImportError if the file fails to be imported
  112. #@throw PluginError if the filename was not valid
  113. def _import_from_init_var(self, varname):
  114. # Read varname
  115. try:
  116. filename = getattr(self.module, varname)
  117. except AttributeError:
  118. msg = "Malformed plugin {plugin}. No {varname} found in __init__.py"
  119. msg = msg.format(
  120. plugin = self.name,
  121. varname = LOADER_FILENAME_VARNAME)
  122. raise PluginError(msg)
  123. #Path are not allowed
  124. if filename != os.path.basename(filename):
  125. msg = "Invalid {varname} content : '{fname}' for plugin {name}"
  126. msg = msg.format(
  127. varname = varname,
  128. fname = filename,
  129. name = self.name)
  130. raise PluginError(msg)
  131. # importing the file in varname
  132. module_name = self.module.__name__+"."+varname
  133. filename = self.path + filename
  134. loader = SourceFileLoader(module_name, filename)
  135. return loader.load_module()
  136. ##@brief Check dependencies of plugin
  137. #@return A list of plugin name to be loaded before
  138. def check_deps(self):
  139. try:
  140. res = getattr(self.module, PLUGIN_DEPS_VARNAME)
  141. except AttributeError:
  142. return list()
  143. result = list()
  144. errors = list()
  145. for plugin_name in res:
  146. try:
  147. result.append(self.get(plugin_name))
  148. except PluginError:
  149. errors.append(plugin_name)
  150. if len(errors) > 0:
  151. raise PluginError( "Bad dependencie for '%s' :"%self.name,
  152. ', '.join(errors))
  153. return result
  154. ##@brief Check if the plugin should be activated
  155. #
  156. #Try to fetch a function called @ref ACTIVATE_METHOD_NAME in __init__.py
  157. #of a plugin. If none found assert that the plugin can be loaded, else
  158. #the method is called. If it returns anything else that True, the plugin
  159. #is noted as not activable
  160. #
  161. # @note Maybe we have to exit everything if a plugin cannot be loaded...
  162. def activable(self):
  163. from lodel import logger
  164. try:
  165. test_fun = getattr(self.module, ACTIVATE_METHOD_NAME)
  166. except AttributeError:
  167. msg = "No %s method found for plugin %s. Assuming plugin is ready to be loaded"
  168. msg %= (ACTIVATE_METHOD_NAME, self.name)
  169. logger.debug(msg)
  170. test_fun = lambda:True
  171. return test_fun()
  172. ##@brief Load a plugin
  173. #
  174. #Loading a plugin means importing a file. The filename is defined in the
  175. #plugin's __init__.py file in a LOADER_FILENAME_VARNAME variable.
  176. #
  177. #The loading process has to take care of other things :
  178. #- loading dependencies (other plugins)
  179. #- check that the plugin can be activated using Plugin.activate() method
  180. #- avoid circular dependencies infinite loop
  181. def _load(self):
  182. if self.loaded:
  183. return
  184. from lodel import logger
  185. #Test that plugin "wants" to be activated
  186. activable = self.activable()
  187. if not(activable is True):
  188. msg = "Plugin %s is not activable : %s"
  189. msg %= (self.name, activable)
  190. raise PluginError(activable)
  191. #Circular dependencie detection
  192. if self.name in self._load_called:
  193. raise PluginError("Circular dependencie in Plugin detected. Abording")
  194. else:
  195. self._load_called.append(self.name)
  196. #Dependencie load
  197. for dependencie in self.check_deps():
  198. activable = dependencie.activable()
  199. if activable is True:
  200. dependencie._load()
  201. else:
  202. msg = "Plugin {plugin_name} not activable because it depends on plugin {dep_name} that is not activable : {reason}"
  203. msg = msg.format(
  204. plugin_name = self.name,
  205. dep_name = dependencie.name,
  206. reason = activable)
  207. #Loading the plugin
  208. try:
  209. self.__loader_module = self._import_from_init_var(LOADER_FILENAME_VARNAME)
  210. except PluginError as e:
  211. raise e
  212. except ImportError as e:
  213. msg = "Broken plugin {plugin} : {expt}"
  214. msg = msg.format(
  215. plugin = self.name,
  216. expt = str(e))
  217. raise PluginError(msg)
  218. logger.debug("Plugin '%s' loaded" % self.name)
  219. self.loaded = True
  220. def loader_module(self):
  221. if not self.loaded:
  222. raise RuntimeError("Plugin %s not loaded yet."%self.name)
  223. return self.__loader_module
  224. ##@brief Call load method on every pre-loaded plugins
  225. #
  226. # Called by loader to trigger hooks registration.
  227. # This method have to avoid circular dependencies infinite loops. For this
  228. # purpose a class attribute _load_called exists.
  229. # @throw PluginError
  230. @classmethod
  231. def load_all(cls):
  232. errors = dict()
  233. cls._load_called = []
  234. for name, plugin in cls._plugin_instances.items():
  235. try:
  236. plugin._load()
  237. except PluginError as e:
  238. errors[name] = e
  239. if len(errors) > 0:
  240. msg = "Errors while loading plugins :"
  241. for name, e in errors.items():
  242. msg += "\n\t%20s : %s" % (name,e)
  243. msg += "\n"
  244. raise PluginError(msg)
  245. from lodel.plugin.hooks import LodelHook
  246. LodelHook.call_hook(
  247. "lodel2_plugins_loaded", cls, cls._plugin_instances)
  248. ##@return a copy of __confspecs attr
  249. @property
  250. def confspecs(self):
  251. return copy.copy(self.__confspecs)
  252. ##@brief Register a new plugin
  253. #
  254. #@param plugin_name str : The plugin name
  255. #@return a Plugin instance
  256. #@throw PluginError
  257. @classmethod
  258. def register(cls, plugin_name):
  259. if plugin_name in cls._plugin_instances:
  260. msg = "Plugin allready registered with same name %s"
  261. msg %= plugin_name
  262. raise PluginError(msg)
  263. plugin = cls(plugin_name)
  264. cls._plugin_instances[plugin_name] = plugin
  265. return plugin
  266. ##@brief Plugins instances accessor
  267. #
  268. #@param plugin_name str: The plugin name
  269. #@return a Plugin instance
  270. #@throw PluginError if plugin not found
  271. @classmethod
  272. def get(cls, plugin_name):
  273. try:
  274. return cls._plugin_instances[plugin_name]
  275. except KeyError:
  276. msg = "No plugin named '%s' loaded"
  277. msg %= plugin_name
  278. raise PluginError(msg)
  279. ##@brief Given a plugin name returns the plugin path
  280. # @param plugin_name str : The plugin name
  281. # @return the plugin directory path
  282. @classmethod
  283. def plugin_path(cls, plugin_name):
  284. cls.started()
  285. try:
  286. return cls.get(plugin_name).path
  287. except PluginError:
  288. pass
  289. path = None
  290. for cur_path in cls._plugin_directories:
  291. plugin_path = os.path.join(cur_path, plugin_name)+'/'
  292. if os.path.isdir(plugin_path):
  293. return plugin_path
  294. raise NameError("No plugin named '%s'" % plugin_name)
  295. @classmethod
  296. def plugin_module_name(cls, plugin_name):
  297. return "%s.%s" % (VIRTUAL_PACKAGE_NAME, plugin_name)
  298. ##@brief Start the Plugin class
  299. #
  300. # Called by Settings.__bootstrap()
  301. #
  302. # This method load path and preload plugins
  303. @classmethod
  304. def start(cls, plugins_directories, plugins):
  305. if cls._plugin_directories is not None:
  306. return
  307. import inspect
  308. self_path = inspect.getsourcefile(Plugin)
  309. default_plugin_path = os.path.abspath(self_path + '../../../../plugins')
  310. if plugins_directories is None:
  311. plugins_directories = list()
  312. plugins_directories += [ default_plugin_path ]
  313. cls._plugin_directories = list(set(plugins_directories))
  314. for plugin_name in plugins:
  315. cls.register(plugin_name)
  316. @classmethod
  317. def started(cls, raise_if_not = True):
  318. res = cls._plugin_directories is not None
  319. if raise_if_not and not res:
  320. raise RuntimeError("Class Plugins is not initialized")
  321. @classmethod
  322. def clear(cls):
  323. if cls._plugin_directories is not None:
  324. cls._plugin_directories = None
  325. if cls._plugin_instances != dict():
  326. cls._plugin_instances = dict()
  327. if cls._load_called != []:
  328. cls._load_called = []
  329. ##@brief Decorator class designed to allow plugins to add custom methods
  330. #to LeObject childs (dyncode objects)
  331. #
  332. class CustomMethod(object):
  333. ##@brief Stores registered custom methods
  334. #
  335. #Key = LeObject child class name
  336. #Value = CustomMethod instance
  337. _custom_methods = dict()
  338. INSTANCE_METHOD = 0
  339. CLASS_METHOD = 1
  340. STATIC_METHOD = 2
  341. ##@brief Decorator constructor
  342. #@param component_name str : the name of the component to enhance
  343. #@param method_name str : the name of the method to inject (if None given
  344. #@param method_type int : take value in one of
  345. #CustomMethod::INSTANCE_METHOD CustomMethod::CLASS_METHOD or
  346. #CustomMethod::STATIC_METHOD
  347. #use the function name
  348. def __init__(self, component_name, method_name = None, method_type=0):
  349. ##@brief The targeted LeObject child class
  350. self._comp_name = component_name
  351. ##@brief The method name
  352. self._method_name = method_name
  353. ##@brief The function (that will be the injected method)
  354. self._fun = None
  355. ##@brief Stores the type of method (instance, class or static)
  356. self._type = int(method_type)
  357. if self._type not in (self.INSTANCE_METHOD, self.CLASS_METHOD,\
  358. self.STATIC_METHOD):
  359. raise ValueError("Excepted value for method_type was one of \
  360. CustomMethod::INSTANCE_METHOD CustomMethod::CLASS_METHOD or \
  361. CustomMethod::STATIC_METHOD, but got %s" % self._type)
  362. ##@brief called just after __init__
  363. #@param fun function : the decorated function
  364. #@param return the function
  365. def __call__(self, fun):
  366. if self._method_name is None:
  367. self._method_name = fun.__name__
  368. if self._comp_name not in self._custom_methods:
  369. self._custom_methods[self._comp_name] = list()
  370. if self._method_name in [ scm._method_name for scm in self._custom_methods[self._comp_name]]:
  371. raise RuntimeError("A method named %s allready registered by \
  372. another plugin : %s" % (
  373. self._method_name,
  374. self._custom_methods[self._comp_name].__module__))
  375. self._fun = fun
  376. self._custom_methods[self._comp_name].append(self)
  377. ##@brief Textual representation
  378. #@return textual representation of the CustomMethod instance
  379. def __repr__(self):
  380. res = "<CustomMethod name={method_name} target={classname} \
  381. source={module_name}.{fun_name}>"
  382. return res.format(
  383. method_name = self._method_name,
  384. classname = self._comp_name,
  385. module_name = self._fun.__module__,
  386. fun_name = self._fun.__name__)
  387. ##@brief Return a well formed method
  388. #
  389. #@note the type of method depends on the _type attribute
  390. #@return a method directly injectable in the target class
  391. def __get_method(self):
  392. if self._type == self.INSTANCE_METHOD:
  393. def custom__get__(self, obj, objtype = None):
  394. return types.MethodType(self, obj, objtype)
  395. setattr(self._fun, '__get__', custom__get__)
  396. return self._fun
  397. elif self._type == self.CLASS_METHOD:
  398. return classmethod(self._fun)
  399. elif self._type == self.STATIC_METHOD:
  400. return staticmethod(self._fun)
  401. else:
  402. raise RuntimeError("Attribute _type is not one of \
  403. CustomMethod::INSTANCE_METHOD CustomMethod::CLASS_METHOD \
  404. CustomMethod::STATIC_METHOD")
  405. ##@brief Handle custom method dynamic injection in LeAPI dyncode
  406. #
  407. #Called by lodel2_dyncode_loaded hook defined at
  408. #lodel.plugin.core_hooks.lodel2_plugin_custom_methods()
  409. #
  410. #@param cls
  411. #@param dynclasses LeObject child classes : List of dynamically generated
  412. #LeObject child classes
  413. @classmethod
  414. def set_registered(cls, dynclasses):
  415. from lodel import logger
  416. dyn_cls_dict = { dc.__name__:dc for dc in dynclasses}
  417. for cls_name, custom_methods in cls._custom_methods.items():
  418. for custom_method in custom_methods:
  419. if cls_name not in dyn_cls_dict:
  420. logger.error("Custom method %s adding fails : No dynamic \
  421. LeAPI objects named %s." % (custom_method, cls_name))
  422. elif custom_method._method_name in dir(dyn_cls_dict[cls_name]):
  423. logger.warning("Overriding existing method '%s' on target \
  424. with %s" % (custom_method._method_name, custom_method))
  425. else:
  426. setattr(
  427. dyn_cls_dict[cls_name],
  428. custom_method._method_name,
  429. custom_method.__get_method())
  430. logger.debug(
  431. "Custom method %s added to target" % custom_method)
  432. ##@page lodel2_plugins Lodel2 plugins system
  433. #
  434. # @par Plugin structure
  435. #A plugin is a package (a folder containing, at least, an __init__.py file.
  436. #This file should expose multiple things :
  437. # - a CONFSPEC variable containing configuration specifications
  438. # - an _activate() method that returns True if the plugin can be activated (
  439. # optionnal)
  440. #