No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

plugins.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  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. ## @package lodel.plugins Lodel2 plugins management
  9. #
  10. # Lodel2 plugins are stored in directories
  11. # A typicall lodel2 plugin directory structure looks like :
  12. # - {{__init__.py}}} containing informations like full_name, authors, licence etc.
  13. # - main.py containing hooks registration etc
  14. # - confspec.py containing a configuration specification dictionary named CONFSPEC
  15. ##@brief The package in which we will load plugins modules
  16. VIRTUAL_PACKAGE_NAME = 'lodel.plugins'
  17. INIT_FILENAME = '__init__.py' # Loaded with settings
  18. CONFSPEC_FILENAME_VARNAME = '__confspec__'
  19. CONFSPEC_VARNAME = 'CONFSPEC'
  20. LOADER_FILENAME_VARNAME = '__loader__'
  21. PLUGIN_DEPS_VARNAME = '__plugin_deps__'
  22. ACTIVATE_METHOD_NAME = '_activate'
  23. class PluginError(Exception):
  24. pass
  25. ##@brief Handle plugins
  26. #
  27. # An instance represent a loaded plugin. Class methods allow to load/preload
  28. # plugins.
  29. #
  30. # Typicall Plugins load sequence is :
  31. # 1. Settings call start method to instanciate all plugins found in confs
  32. # 2. Settings fetch all confspecs
  33. # 3. the loader call load_all to register hooks etc
  34. class Plugin(object):
  35. ##@brief Stores plugin directories paths
  36. _plugin_directories = None
  37. ##@brief Stores Plugin instances indexed by name
  38. _plugin_instances = dict()
  39. ##@brief Attribute used by load_all and load methods to detect circular
  40. #dependencies
  41. _load_called = []
  42. ##@brief Plugin class constructor
  43. #
  44. # Called by setting in early stage of lodel2 boot sequence using classmethod
  45. # register
  46. #
  47. # @param plugin_name str : plugin name
  48. # @throw PluginError
  49. def __init__(self, plugin_name):
  50. self.started()
  51. self.name = plugin_name
  52. self.path = self.plugin_path(plugin_name)
  53. ##@brief Stores the plugin module
  54. self.module = None
  55. ##@breif Stores the plugin loader module
  56. self.__loader_module = None
  57. self.__confspecs = dict()
  58. self.loaded = False
  59. # Importing __init__.py
  60. plugin_module = '%s.%s' % (VIRTUAL_PACKAGE_NAME,
  61. plugin_name)
  62. init_source = self.path + INIT_FILENAME
  63. try:
  64. loader = SourceFileLoader(plugin_module, init_source)
  65. self.module = loader.load_module()
  66. except (ImportError,FileNotFoundError) as e:
  67. raise PluginError("Failed to load plugin '%s'. It seems that the plugin name is not valid or the plugin do not exists" % plugin_name)
  68. # loading confspecs
  69. try:
  70. # Loading confspec directly from __init__.py
  71. self.__confspecs = getattr(self.module, CONFSPEC_VARNAME)
  72. except AttributeError:
  73. # Loading file in __confspec__ var in __init__.py
  74. try:
  75. module = self._import_from_init_var(CONFSPEC_FILENAME_VARNAME)
  76. except AttributeError:
  77. msg = "Malformed plugin {plugin} . No {varname} not {filevar} found in __init__.py"
  78. msg = msg.format(
  79. plugin = self.name,
  80. varname = CONFSPEC_VARNAME,
  81. filevar = CONFSPEC_FILENAME_VARNAME)
  82. raise PluginError(msg)
  83. except ImportError as e:
  84. msg = "Broken plugin {plugin} : {expt}"
  85. msg = msg.format(
  86. plugin = self.name,
  87. expt = str(e))
  88. raise PluginError(msg)
  89. try:
  90. # loading confpsecs from file
  91. self.__confspecs = getattr(module, CONFSPEC_VARNAME)
  92. except AttributeError:
  93. msg = "Broken plugin. {varname} not found in '{filename}'"
  94. msg = msg.format(
  95. varname = CONFSPEC_VARNAME,
  96. filename = confspec_filename)
  97. raise PluginError(msg)
  98. ##@brief Try to import a file from a variable in __init__.py
  99. #@param varname str : The variable name
  100. #@return loaded module
  101. #@throw AttributeError if varname not found
  102. #@throw ImportError if the file fails to be imported
  103. #@throw PluginError if the filename was not valid
  104. def _import_from_init_var(self, varname):
  105. # Read varname
  106. filename = getattr(self.module, varname)
  107. #Path are not allowed
  108. if filename != os.path.basename(filename):
  109. msg = "Invalid {varname} content : '{fname}' for plugin {name}"
  110. msg = msg.format(
  111. varname = varname,
  112. fname = filename,
  113. name = self.name)
  114. raise PluginError(msg)
  115. # importing the file in varname
  116. module_name = self.module.__name__+"."+varname
  117. filename = self.path + filename
  118. loader = SourceFileLoader(module_name, filename)
  119. return loader.load_module()
  120. ##@brief Check dependencies of plugin
  121. #@return A list of plugin name to be loaded before
  122. def check_deps(self):
  123. try:
  124. res = getattr(self.module, PLUGIN_DEPS_VARNAME)
  125. except AttributeError:
  126. return list()
  127. result = list()
  128. errors = list()
  129. for plugin_name in res:
  130. try:
  131. result.append(self.get(plugin_name))
  132. except PluginError:
  133. errors.append(plugin_name)
  134. if len(errors) > 0:
  135. raise PluginError( "Bad dependencie for '%s' :"%self.name,
  136. ', '.join(errors))
  137. return result
  138. ##@brief Check if the plugin should be activated
  139. #
  140. #Try to fetch a function called @ref ACTIVATE_METHOD_NAME in __init__.py
  141. #of a plugin. If none found assert that the plugin can be loaded, else
  142. #the method is called. If it returns anything else that True, the plugin
  143. #is noted as not activable
  144. #
  145. # @note Maybe we have to exit everything if a plugin cannot be loaded...
  146. def activable(self):
  147. from lodel import logger
  148. try:
  149. test_fun = getattr(self.module, ACTIVATE_METHOD_NAME)
  150. except AttributeError:
  151. msg = "No %s method found for plugin %s. Assuming plugin is ready to be loaded"
  152. msg %= (ACTIVATE_METHOD_NAME, self.name)
  153. logger.debug(msg)
  154. test_fun = lambda:True
  155. return test_fun()
  156. ##@brief Load a plugin
  157. #
  158. #Loading a plugin means importing a file. The filename is defined in the
  159. #plugin's __init__.py file in a LOADER_FILENAME_VARNAME variable.
  160. #
  161. #The loading process has to take care of other things :
  162. #- loading dependencies (other plugins)
  163. #- check that the plugin can be activated using Plugin.activate() method
  164. #- avoid circular dependencies infinite loop
  165. def _load(self):
  166. if self.loaded:
  167. return
  168. from lodel import logger
  169. #Test that plugin "wants" to be activated
  170. activable = self.activable()
  171. if not(activable is True):
  172. msg = "Plugin %s is not activable : %s"
  173. msg %= (self.name, activable)
  174. raise PluginError(activable)
  175. #Circular dependencie detection
  176. if self.name in self._load_called:
  177. raise PluginError("Circular dependencie in Plugin detected. Abording")
  178. else:
  179. self._load_called.append(self.name)
  180. #Dependencie load
  181. for dependencie in self.check_deps():
  182. activable = dependencie.activable()
  183. if activable is True:
  184. dependencie._load()
  185. else:
  186. msg = "Plugin {plugin_name} not activable because it depends on plugin {dep_name} that is not activable : {reason}"
  187. msg = msg.format(
  188. plugin_name = self.name,
  189. dep_name = dependencie.name,
  190. reason = activable)
  191. #Loading the plugin
  192. try:
  193. self.__loader_module = self._import_from_init_var(LOADER_FILENAME_VARNAME)
  194. except AttributeError:
  195. msg = "Malformed plugin {plugin}. No {varname} found in __init__.py"
  196. msg = msg.format(
  197. plugin = self.name,
  198. varname = LOADER_FILENAME_VARNAME)
  199. raise PluginError(msg)
  200. except ImportError as e:
  201. msg = "Broken plugin {plugin} : {expt}"
  202. msg = msg.format(
  203. plugin = self.name,
  204. expt = str(e))
  205. raise PluginError(msg)
  206. logger.debug("Plugin '%s' loaded" % self.name)
  207. self.loaded = True
  208. def loader_module(self):
  209. if not self.loaded:
  210. raise RuntimeError("Plugin %s not loaded yet."%self.name)
  211. return self.__loader_module
  212. ##@brief Call load method on every pre-loaded plugins
  213. #
  214. # Called by loader to trigger hooks registration.
  215. # This method have to avoid circular dependencies infinite loops. For this
  216. # purpose a class attribute _load_called exists.
  217. # @throw PluginError
  218. @classmethod
  219. def load_all(cls):
  220. errors = dict()
  221. cls._load_called = []
  222. for name, plugin in cls._plugin_instances.items():
  223. try:
  224. plugin._load()
  225. except PluginError as e:
  226. errors[name] = e
  227. if len(errors) > 0:
  228. msg = "Errors while loading plugins :"
  229. for name, e in errors.items():
  230. msg += "\n\t%20s : %s" % (name,e)
  231. msg += "\n"
  232. raise PluginError(msg)
  233. from lodel.plugin.hooks import LodelHook
  234. LodelHook.call_hook(
  235. "lodel2_plugins_loaded", cls, cls._plugin_instances)
  236. ##@return a copy of __confspecs attr
  237. @property
  238. def confspecs(self):
  239. return copy.copy(self.__confspecs)
  240. ##@brief Register a new plugin
  241. #
  242. #@param plugin_name str : The plugin name
  243. #@return a Plugin instance
  244. #@throw PluginError
  245. @classmethod
  246. def register(cls, plugin_name):
  247. if plugin_name in cls._plugin_instances:
  248. msg = "Plugin allready registered with same name %s"
  249. msg %= plugin_name
  250. raise PluginError(msg)
  251. plugin = cls(plugin_name)
  252. cls._plugin_instances[plugin_name] = plugin
  253. return plugin
  254. ##@brief Plugins instances accessor
  255. #
  256. #@param plugin_name str: The plugin name
  257. #@return a Plugin instance
  258. #@throw PluginError if plugin not found
  259. @classmethod
  260. def get(cls, plugin_name):
  261. try:
  262. return cls._plugin_instances[plugin_name]
  263. except KeyError:
  264. msg = "No plugin named '%s' loaded"
  265. msg %= plugin_name
  266. raise PluginError(msg)
  267. ##@brief Given a plugin name returns the plugin path
  268. # @param plugin_name str : The plugin name
  269. # @return the plugin directory path
  270. @classmethod
  271. def plugin_path(cls, plugin_name):
  272. cls.started()
  273. try:
  274. return cls.get(plugin_name).path
  275. except PluginError:
  276. pass
  277. path = None
  278. for cur_path in cls._plugin_directories:
  279. plugin_path = os.path.join(cur_path, plugin_name)+'/'
  280. if os.path.isdir(plugin_path):
  281. return plugin_path
  282. raise NameError("No plugin named '%s'" % plugin_name)
  283. @classmethod
  284. def plugin_module_name(cls, plugin_name):
  285. return "%s.%s" % (VIRTUAL_PACKAGE_NAME, plugin_name)
  286. ##@brief Start the Plugin class
  287. #
  288. # Called by Settings.__bootstrap()
  289. #
  290. # This method load path and preload plugins
  291. @classmethod
  292. def start(cls, plugins_directories, plugins):
  293. if cls._plugin_directories is not None:
  294. return
  295. import inspect
  296. self_path = inspect.getsourcefile(Plugin)
  297. default_plugin_path = os.path.abspath(self_path + '../../../../plugins')
  298. if plugins_directories is None:
  299. plugins_directories = list()
  300. plugins_directories += [ default_plugin_path ]
  301. cls._plugin_directories = list(set(plugins_directories))
  302. for plugin_name in plugins:
  303. cls.register(plugin_name)
  304. @classmethod
  305. def started(cls, raise_if_not = True):
  306. res = cls._plugin_directories is not None
  307. if raise_if_not and not res:
  308. raise RuntimeError("Class Plugins is not initialized")
  309. @classmethod
  310. def clear(cls):
  311. if cls._plugin_directories is not None:
  312. cls._plugin_directories = None
  313. if cls._plugin_instances != dict():
  314. cls._plugin_instances = dict()
  315. if cls._load_called != []:
  316. cls._load_called = []
  317. ##@page lodel2_plugins Lodel2 plugins system
  318. #
  319. # @par Plugin structure
  320. #A plugin is a package (a folder containing, at least, an __init__.py file.
  321. #This file should expose multiple things :
  322. # - a CONFSPEC variable containing configuration specifications
  323. # - an _activate() method that returns True if the plugin can be activated (
  324. # optionnal)
  325. #