|
@@ -25,6 +25,16 @@ LodelContext.expose_modules(globals(), {
|
25
|
25
|
# - {{__init__.py}}} containing informations like full_name, authors, licence etc.
|
26
|
26
|
# - main.py containing hooks registration etc
|
27
|
27
|
# - confspec.py containing a configuration specification dictionary named CONFSPEC
|
|
28
|
+#
|
|
29
|
+# All plugins are expected to be found in multiple locations :
|
|
30
|
+# - in the lodel package (lodel.plugins)
|
|
31
|
+# - in the context directorie in a plugins/ dir (symlink to lodel.plugins) <-
|
|
32
|
+#this is obsolete now, since we enforce ALL plugins to be in the lodel package
|
|
33
|
+#
|
|
34
|
+#@todo Check if the symlink in the lodelcontext dir is obsolete !!!
|
|
35
|
+#@warning The plugins dir is at two locations : in lodel package and in
|
|
36
|
+#instance directory. Some stuff seems to still needs plugins to be in
|
|
37
|
+#the instance directory but it seems to be a really bad idea...
|
28
|
38
|
|
29
|
39
|
##@defgroup plugin_init_specs Plugins __init__.py specifications
|
30
|
40
|
#@ingroup lodel2_plugins
|
|
@@ -52,10 +62,8 @@ LOADER_FILENAME_VARNAME = '__loader__'
|
52
|
62
|
PLUGIN_DEPS_VARNAME = '__plugin_deps__'
|
53
|
63
|
##@brief Name of the optionnal activate method
|
54
|
64
|
ACTIVATE_METHOD_NAME = '_activate'
|
55
|
|
-##@brief Discover stage cache filename
|
56
|
|
-DISCOVER_CACHE_FILENAME = '.plugin_discover_cache.json'
|
57
|
65
|
##@brief Default & failover value for plugins path list
|
58
|
|
-DEFAULT_PLUGINS_PATH_LIST = [os.path.join(LodelContext.context_dir(),'plugins')]
|
|
66
|
+PLUGINS_PATH = os.path.join(LodelContext.context_dir(),'plugins')
|
59
|
67
|
|
60
|
68
|
##@brief List storing the mandatory variables expected in a plugin __init__.py
|
61
|
69
|
#file
|
|
@@ -87,11 +95,19 @@ class PluginVersion(object):
|
87
|
95
|
if len(args) == 1:
|
88
|
96
|
arg = args[0]
|
89
|
97
|
if isinstance(arg, str):
|
|
98
|
+ #Casting from string to version numbers
|
90
|
99
|
spl = arg.split('.')
|
91
|
100
|
invalid = False
|
92
|
101
|
if len(spl) > 3:
|
93
|
102
|
raise PluginError("The string '%s' is not a valid plugin \
|
94
|
103
|
version number" % arg)
|
|
104
|
+ if len(spl) < 3:
|
|
105
|
+ spl += [ 0 for _ in range(3-len(spl))]
|
|
106
|
+ try:
|
|
107
|
+ self.__version = [int(s) for s in spl]
|
|
108
|
+ except (ValueError, TypeError):
|
|
109
|
+ raise PluginError("The string '%s' is not a valid lodel2 \
|
|
110
|
+plugin version number" % arg)
|
95
|
111
|
else:
|
96
|
112
|
try:
|
97
|
113
|
if len(arg) >= 1:
|
|
@@ -99,8 +115,8 @@ version number" % arg)
|
99
|
115
|
raise PluginError("Expected maximum 3 value to \
|
100
|
116
|
create a plugin version number but found '%s' as argument" % arg)
|
101
|
117
|
for i, v in enumerate(arg):
|
102
|
|
- self.__version[i] = arg[i]
|
103
|
|
- except TypeError:
|
|
118
|
+ self.__version[i] = int(arg[i])
|
|
119
|
+ except (TypeError, ValueError):
|
104
|
120
|
raise PluginError("Unable to convert argument into plugin \
|
105
|
121
|
version number" % arg)
|
106
|
122
|
elif len(args) > 3:
|
|
@@ -108,8 +124,8 @@ version number" % arg)
|
108
|
124
|
but %d arguments found" % len(args))
|
109
|
125
|
else:
|
110
|
126
|
for i,v in enumerate(args):
|
111
|
|
- self.__version[i] = v
|
112
|
|
-
|
|
127
|
+ self.__version[i] = int(v)
|
|
128
|
+
|
113
|
129
|
##@brief Property to access major version number
|
114
|
130
|
@property
|
115
|
131
|
def major(self):
|
|
@@ -149,8 +165,11 @@ a PluginVerison instance" % other)
|
149
|
165
|
raise LodelFatalError("Invalid comparison callback given \
|
150
|
166
|
to generic PluginVersion comparison function : '%s'" % cmp_fun_name)
|
151
|
167
|
for property_name in self.PROPERTY_LIST:
|
152
|
|
- if not cmpfun(getattr(self, pname), getattr(other, pname)):
|
153
|
|
- return False
|
|
168
|
+ if not cmpfun(
|
|
169
|
+ getattr(self, property_name),
|
|
170
|
+ getattr(other, property_name)):
|
|
171
|
+ if property_name == self.PROPERTY_LIST[-1]:
|
|
172
|
+ return False
|
154
|
173
|
return True
|
155
|
174
|
|
156
|
175
|
def __lt__(self, other):
|
|
@@ -175,7 +194,7 @@ to generic PluginVersion comparison function : '%s'" % cmp_fun_name)
|
175
|
194
|
return '%d.%d.%d' % tuple(self.__version)
|
176
|
195
|
|
177
|
196
|
def __repr__(self):
|
178
|
|
- return {'major': self.major, 'minor': self.minor,
|
|
197
|
+ return "%s" % {'major': self.major, 'minor': self.minor,
|
179
|
198
|
'revision': self.revision}
|
180
|
199
|
|
181
|
200
|
##@brief Plugin metaclass that allows to "catch" child class declaration
|
|
@@ -240,9 +259,6 @@ class MetaPlugType(type):
|
240
|
259
|
# 3. the loader call load_all to register hooks etc
|
241
|
260
|
class Plugin(object, metaclass=MetaPlugType):
|
242
|
261
|
|
243
|
|
- ##@brief Stores plugin directories paths
|
244
|
|
- _plugin_directories = None
|
245
|
|
-
|
246
|
262
|
##@brief Stores Plugin instances indexed by name
|
247
|
263
|
_plugin_instances = dict()
|
248
|
264
|
|
|
@@ -253,9 +269,6 @@ class Plugin(object, metaclass=MetaPlugType):
|
253
|
269
|
##@brief Attribute that stores plugins list from discover cache file
|
254
|
270
|
_plugin_list = None
|
255
|
271
|
|
256
|
|
- ##@brief Store dict representation of discover cache content
|
257
|
|
- _discover_cache = None
|
258
|
|
-
|
259
|
272
|
#@brief Designed to store, in child classes, the confspec indicating \
|
260
|
273
|
#where plugin list is stored
|
261
|
274
|
_plist_confspecs = None
|
|
@@ -295,8 +308,7 @@ class Plugin(object, metaclass=MetaPlugType):
|
295
|
308
|
self.loaded = False
|
296
|
309
|
|
297
|
310
|
# Importing __init__.py infos in it
|
298
|
|
- plugin_module = '%s.%s' % (VIRTUAL_PACKAGE_NAME,
|
299
|
|
- plugin_name)
|
|
311
|
+ plugin_module = self.module_name()
|
300
|
312
|
self.module = LodelContext.module(plugin_module)
|
301
|
313
|
|
302
|
314
|
# loading confspecs
|
|
@@ -406,6 +418,19 @@ name differ from the one found in plugin's init file"
|
406
|
418
|
module_name = self_modname+"."+base_mod
|
407
|
419
|
return importlib.import_module(module_name)
|
408
|
420
|
|
|
421
|
+ ##@brief Return associated module name
|
|
422
|
+ def module_name(self):
|
|
423
|
+ if not self.path.startswith('./plugins'):
|
|
424
|
+ raise PluginError("Bad path for plugin %s : %s" % (
|
|
425
|
+ self.name, self.path))
|
|
426
|
+ mod_name = ''
|
|
427
|
+ pathbuff = self.path
|
|
428
|
+ while pathbuff != '.':
|
|
429
|
+ mod_name = os.path.basename(pathbuff) + '.' + mod_name
|
|
430
|
+ pathbuff = os.path.dirname(pathbuff)
|
|
431
|
+ #removing trailing '.' and add leading lodel.
|
|
432
|
+ return 'lodel.'+mod_name[:-1]
|
|
433
|
+
|
409
|
434
|
##@brief Check dependencies of plugin
|
410
|
435
|
#@return A list of plugin name to be loaded before
|
411
|
436
|
def check_deps(self):
|
|
@@ -561,35 +586,6 @@ name differ from the one found in plugin's init file"
|
561
|
586
|
confspec_append(res, plcs)
|
562
|
587
|
return res
|
563
|
588
|
|
564
|
|
- ##@brief Attempt to read plugin discover cache
|
565
|
|
- #@note If no cache yet make a discover with default plugin directory
|
566
|
|
- #@return a dict (see @ref _discover() )
|
567
|
|
- @classmethod
|
568
|
|
- def plugin_cache(cls):
|
569
|
|
- if cls._discover_cache is None:
|
570
|
|
- if not os.path.isfile(DISCOVER_CACHE_FILENAME):
|
571
|
|
- cls.discover()
|
572
|
|
- with open(DISCOVER_CACHE_FILENAME) as pdcache_fd:
|
573
|
|
- res = json.load(pdcache_fd)
|
574
|
|
- #Check consistency of loaded cache
|
575
|
|
- if 'path_list' not in res:
|
576
|
|
- raise LodelFatalError("Malformed plugin's discover cache file \
|
577
|
|
-: '%s'. Unable to find plugin's paths list." % DISCOVER_CACHE_FILENAME)
|
578
|
|
- expected_keys = ['type', 'path', 'version']
|
579
|
|
- for pname in res['plugins']:
|
580
|
|
- for ekey in expected_keys:
|
581
|
|
- if ekey not in res['plugins'][pname]:
|
582
|
|
- #Bad cache !
|
583
|
|
- logger.warning("Malformed plugin's discover cache \
|
584
|
|
-file : '%s'. Running discover again..." % DISCOVER_CACHE_FILENAME)
|
585
|
|
- cls._discover_cache = cls.discover(res['path_list'])
|
586
|
|
- break
|
587
|
|
- else:
|
588
|
|
- #The cache we just read was OK
|
589
|
|
- cls._discover_cache = res
|
590
|
|
-
|
591
|
|
- return cls._discover_cache
|
592
|
|
-
|
593
|
589
|
##@brief Register a new plugin
|
594
|
590
|
#
|
595
|
591
|
#@param plugin_name str : The plugin name
|
|
@@ -602,11 +598,10 @@ file : '%s'. Running discover again..." % DISCOVER_CACHE_FILENAME)
|
602
|
598
|
msg %= plugin_name
|
603
|
599
|
raise PluginError(msg)
|
604
|
600
|
#Here we check that previous discover found a plugin with that name
|
605
|
|
- pdcache = cls.plugin_cache()
|
606
|
|
- if plugin_name not in pdcache['plugins']:
|
|
601
|
+ pdcache = cls.discover()
|
|
602
|
+ if plugin_name not in pdcache:
|
607
|
603
|
raise PluginError("No plugin named %s found" % plugin_name)
|
608
|
|
- pinfos = pdcache['plugins'][plugin_name]
|
609
|
|
- ptype = pinfos['type']
|
|
604
|
+ ptype = pdcache[plugin_name]['type']
|
610
|
605
|
pcls = MetaPlugType.type_from_name(ptype)
|
611
|
606
|
plugin = pcls(plugin_name)
|
612
|
607
|
cls._plugin_instances[plugin_name] = plugin
|
|
@@ -632,7 +627,6 @@ file : '%s'. Running discover again..." % DISCOVER_CACHE_FILENAME)
|
632
|
627
|
# @return the plugin directory path
|
633
|
628
|
@classmethod
|
634
|
629
|
def plugin_path(cls, plugin_name):
|
635
|
|
-
|
636
|
630
|
plist = cls.plugin_list()
|
637
|
631
|
if plugin_name not in plist:
|
638
|
632
|
raise PluginError("No plugin named '%s' found" % plugin_name)
|
|
@@ -649,8 +643,11 @@ file : '%s'. Running discover again..." % DISCOVER_CACHE_FILENAME)
|
649
|
643
|
#This module name is the "virtual" module where we imported the plugin.
|
650
|
644
|
#
|
651
|
645
|
#Typically composed like VIRTUAL_PACKAGE_NAME.PLUGIN_NAME
|
|
646
|
+ #@warning Brokes subdire feature
|
652
|
647
|
#@param plugin_name str : a plugin name
|
653
|
648
|
#@return a string representing a module name
|
|
649
|
+ #@todo fix broken subdir capabilitie ( @see module_name() )
|
|
650
|
+ #@todo check if used, else delete it
|
654
|
651
|
@classmethod
|
655
|
652
|
def plugin_module_name(cls, plugin_name):
|
656
|
653
|
return "%s.%s" % (VIRTUAL_PACKAGE_NAME, plugin_name)
|
|
@@ -668,8 +665,6 @@ file : '%s'. Running discover again..." % DISCOVER_CACHE_FILENAME)
|
668
|
665
|
##@brief Attempt to "restart" the Plugin class
|
669
|
666
|
@classmethod
|
670
|
667
|
def clear(cls):
|
671
|
|
- if cls._plugin_directories is not None:
|
672
|
|
- cls._plugin_directories = None
|
673
|
668
|
if cls._plugin_instances != dict():
|
674
|
669
|
cls._plugin_instances = dict()
|
675
|
670
|
if cls._load_called != []:
|
|
@@ -682,20 +677,18 @@ file : '%s'. Running discover again..." % DISCOVER_CACHE_FILENAME)
|
682
|
677
|
pass
|
683
|
678
|
|
684
|
679
|
##@brief Reccursively walk throught paths to find plugin, then stores
|
685
|
|
- #found plugin in a file...
|
686
|
|
- #@param paths list : list of directory paths
|
687
|
|
- #@param no_cache bool : if true only return a list of found plugins
|
688
|
|
- #without modifying the cache file
|
689
|
|
- #@return a dict {'path_list': [...], 'plugins': { see @ref _discover }}
|
690
|
|
- #@todo add max_depth and symlink following options
|
|
680
|
+ #found plugin in a static var
|
|
681
|
+ #
|
|
682
|
+ #Found plugins are stored in cls._plugin_list
|
|
683
|
+ #@note The discover is run only if no cached datas are found
|
|
684
|
+ #@return a list of dict with plugin infos { see @ref _discover }
|
|
685
|
+ #@todo add max_depth and no symlink following feature
|
691
|
686
|
@classmethod
|
692
|
|
- def discover(cls, paths = None, no_cache = False):
|
|
687
|
+ def discover(cls):
|
|
688
|
+ if cls._plugin_list is not None:
|
|
689
|
+ return cls._plugin_list
|
693
|
690
|
logger.info("Running plugin discover")
|
694
|
|
- if paths is None:
|
695
|
|
- paths = DEFAULT_PLUGINS_PATH_LIST
|
696
|
|
- tmp_res = []
|
697
|
|
- for path in paths:
|
698
|
|
- tmp_res += cls._discover(path)
|
|
691
|
+ tmp_res = cls._discover(PLUGINS_PATH)
|
699
|
692
|
#Formating and dedoubloning result
|
700
|
693
|
result = dict()
|
701
|
694
|
for pinfos in tmp_res:
|
|
@@ -707,11 +700,7 @@ file : '%s'. Running discover again..." % DISCOVER_CACHE_FILENAME)
|
707
|
700
|
else:
|
708
|
701
|
#dropped
|
709
|
702
|
pass
|
710
|
|
- result = {'path_list': paths, 'plugins': result}
|
711
|
|
- #Writing to cache
|
712
|
|
- if not no_cache:
|
713
|
|
- with open(DISCOVER_CACHE_FILENAME, 'w+') as pdcache:
|
714
|
|
- pdcache.write(json.dumps(result))
|
|
703
|
+ cls._plugin_list = result
|
715
|
704
|
return result
|
716
|
705
|
|
717
|
706
|
##@brief Return discover result
|
|
@@ -722,17 +711,6 @@ file : '%s'. Running discover again..." % DISCOVER_CACHE_FILENAME)
|
722
|
711
|
#from the plugin's discover cache
|
723
|
712
|
@classmethod
|
724
|
713
|
def plugin_list(cls, refresh = False):
|
725
|
|
- try:
|
726
|
|
- infos = cls._load_discover_cache()
|
727
|
|
- path_list = infos['path_list']
|
728
|
|
- except PluginError:
|
729
|
|
- refresh = True
|
730
|
|
- path_list = DEFAULT_PLUGINS_PATH_LIST
|
731
|
|
-
|
732
|
|
- if cls._plugin_list is None or refresh:
|
733
|
|
- if not os.path.isfile(DISCOVER_CACHE_FILENAME) or refresh:
|
734
|
|
- infos = cls.discover(path_list)
|
735
|
|
- cls._plugin_list = infos['plugins']
|
736
|
714
|
return cls._plugin_list
|
737
|
715
|
|
738
|
716
|
##@brief Return a list of child Class Plugin
|
|
@@ -740,32 +718,20 @@ file : '%s'. Running discover again..." % DISCOVER_CACHE_FILENAME)
|
740
|
718
|
def plugin_types(cls):
|
741
|
719
|
return MetaPlugType.all_types()
|
742
|
720
|
|
743
|
|
- ##@brief Attempt to open and load plugin discover cache
|
744
|
|
- #@return discover cache
|
745
|
|
- #@throw PluginError when open or load fails
|
746
|
|
- @classmethod
|
747
|
|
- def _load_discover_cache(cls):
|
748
|
|
- try:
|
749
|
|
- pdcache = open(DISCOVER_CACHE_FILENAME, 'r')
|
750
|
|
- except Exception as e:
|
751
|
|
- msg = "Unable to open discover cache : %s"
|
752
|
|
- msg %= e
|
753
|
|
- raise PluginError(msg)
|
754
|
|
- try:
|
755
|
|
- res = json.load(pdcache)
|
756
|
|
- except Exception as e:
|
757
|
|
- msg = "Unable to load discover cache : %s"
|
758
|
|
- msg %= e
|
759
|
|
- raise PluginError(msg)
|
760
|
|
- pdcache.close()
|
761
|
|
- return res
|
762
|
|
-
|
763
|
721
|
##@brief Check if a directory is a plugin module
|
764
|
722
|
#@param path str : path to check
|
|
723
|
+ #@param assert_in_package bool : if False didn't check that path is
|
|
724
|
+ #a subdir of PLUGINS_PATH
|
765
|
725
|
#@return a dict with name, version and path if path is a plugin module, else False
|
766
|
726
|
@classmethod
|
767
|
|
- def dir_is_plugin(cls, path):
|
|
727
|
+ def dir_is_plugin(cls, path, assert_in_package = True):
|
768
|
728
|
log_msg = "%s is not a plugin directory because : " % path
|
|
729
|
+ if assert_in_package:
|
|
730
|
+ #Check that path is a subdir of PLUGINS_PATH
|
|
731
|
+ abspath = os.path.abspath(path)
|
|
732
|
+ if not abspath.startswith(os.path.abspath(PLUGINS_PATH)):
|
|
733
|
+ raise PluginError(
|
|
734
|
+ "%s is not a subdir of %s" % log_msg, PLUGINS_PATH)
|
769
|
735
|
#Checks that path exists
|
770
|
736
|
if not os.path.isdir(path):
|
771
|
737
|
raise ValueError(
|
|
@@ -805,7 +771,7 @@ file : '%s'. Running discover again..." % DISCOVER_CACHE_FILENAME)
|
805
|
771
|
ptype = DEFAULT_PLUGIN_TYPE
|
806
|
772
|
pname = getattr(initmod, PLUGIN_NAME_VARNAME)
|
807
|
773
|
return {'name': pname,
|
808
|
|
- 'version': pversion,
|
|
774
|
+ 'version': PluginVersion(pversion),
|
809
|
775
|
'path': path,
|
810
|
776
|
'type': ptype}
|
811
|
777
|
|
|
@@ -857,7 +823,8 @@ file : '%s'. Running discover again..." % DISCOVER_CACHE_FILENAME)
|
857
|
823
|
#Check if it is a plugin directory
|
858
|
824
|
test_result = cls.dir_is_plugin(f_path)
|
859
|
825
|
if not (test_result is False):
|
860
|
|
- logger.info("Plugin found in %s" % f_path)
|
|
826
|
+ logger.info("Plugin '%s' found in %s" % (
|
|
827
|
+ test_result['name'],f_path))
|
861
|
828
|
res.append(test_result)
|
862
|
829
|
else:
|
863
|
830
|
to_explore.append(f_path)
|