Nav apraksta
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

plugins.py 36KB

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