1
2 """
3
4 @author: Fabio Erculiani <lxnay@sabayonlinux.org>
5 @contact: lxnay@sabayonlinux.org
6 @copyright: Fabio Erculiani
7 @license: GPL-2
8
9 B{Entropy Framework core module}.
10
11 This module contains base classes used by entropy.client,
12 entropy.server and entropy.services.
13
14 "Singleton" is a class that is inherited from singleton objects.
15
16 """
17 import sys
18 import os
19 import inspect
20 from entropy.const import etpConst
21
23
24 """
25 If your class wants to become a sexy Singleton,
26 subclass this and replace __init__ with init_singleton.
27 Your subclass can expose a method called "is_destroyed()" that
28 returns a bool stating if singleton instance has been destroyed.
29 """
30
32
33 singleton = getattr(cls, '__singleton__', None)
34 if singleton is not None:
35 destroyed = getattr(singleton, 'is_destroyed', None)
36 if destroyed is not None:
37 if not destroyed():
38 return singleton
39 cls.__singleton__ = None
40 else:
41 return singleton
42
43 singleton = object.__new__(cls)
44 singleton.init_singleton(*args, **kwds)
45 cls.__singleton__ = singleton
46 return singleton
47
49 """
50 This is a fake method, necessary for Python 3.
51 """
52 pass
53
55
56 """
57 This is a base class for handling a map of plugin objects by providing
58 generic add/remove functions.
59 """
60
63
65 """
66 Return a copy of the internal dictionary that contains the currently
67 stored plugins map.
68
69 @return: stored plugins map
70 @rtype: dict
71 """
72 return self.__plugins.copy()
73
75 """
76 Drop all the currently stored plugins from storage.
77 """
78 self.__plugins.clear()
79
81 """
82 This method lets you add a plugin to the store.
83
84 @param plugin_id: plugin identifier
85 @type plugin_id: string
86 @param plugin_object: valid SystemSettingsPlugin instance
87 @type plugin_object: any Python object
88 """
89 self.__plugins[plugin_id] = plugin_object
90
92 """
93 This method lets you remove previously added plugin objects via
94 identifiers.
95
96 @param plugin_id: plugin identifier
97 @type plugin_id: basestring
98 """
99 del self.__plugins[plugin_id]
100
101
103
104 """
105 Generic Entropy Components Plugin Factory (loader).
106 """
107
108 _PLUGIN_SUFFIX = "_plugin"
109 _PYTHON_EXTENSION = ".py"
110
111 - def __init__(self, base_plugin_class, plugin_package_module,
112 default_plugin_name = None, fallback_plugin_name = None,
113 egg_entry_point_group = None):
114 """
115 Entropy Generic Plugin Factory constructor.
116 MANDATORY: every plugin module/package(name) must end with _plugin
117 suffix.
118
119 Base plugin classes must have the following class attributes set:
120
121 - BASE_PLUGIN_API_VERSION: integer describing API revision in use
122 in class
123
124 Subclasses of Base plugin class must have the following class
125 attributes set:
126
127 - PLUGIN_API_VERSION: integer describing the currently implemented
128 plugin API revision, must match with BASE_PLUGIN_API_VERSION
129 above otherwise plugin won't be loaded and a warning will be
130 printed.
131
132 Moreover, plugin classes must be "Python new-style classes", otherwise
133 parser won't be able to determine if classes have subclasses and thus
134 pick the proper object (one with no subclasses!!).
135 See: http://www.python.org/doc/newstyle -- in other words, you have
136 to inherit the built-in "object" class (yeah, it's called object).
137 So, even if using normal classes could work, if you start doing nasty
138 things (nested inherittance of plugin classes), behaviour cannot
139 be guaranteed.
140 If it's not clear, let me repeat once again, valid plugin classes
141 must not have subclasses around! Think about it, it's an obvious thing.
142
143 If plugin class features a "PLUGIN_DISABLED" class attribute with
144 a boolean value of True, such plugin will be ignored.
145
146 If egg_entry_point_group is specified, Python Egg support is enabled
147 and classes are loaded via this infrastructure.
148 NOTE: if egg_entry_point_group is set, you NEED the setuptools package.
149
150 @param base_plugin_class: Base EntropyPlugin-based class that valid
151 plugin classes must inherit from.
152 @type base_plugin_class: class
153 @param plugin_package_module: every plugin repository must work as
154 Python package, the value of this argument must be a valid
155 Python package module that can be scanned looking for valid
156 Entropy Plugin classes.
157 @type plugin_package_module: Python module
158 @keyword default_plugin_name: identifier of the default plugin to load
159 @type default_plugin_name: string
160 @keyword fallback_plugin_name: identifier of the fallback plugin to load
161 if default is not available
162 @type fallback_plugin_name: string
163 @keyword egg_entry_point_group: valid Python Egg entry point group, in
164 this case, Python Egg support is used
165 @type egg_entry_point_group: string
166 @raise AttributeError: when passed plugin_package_module is not a
167 valid Python package module
168 """
169 self.__modfile = plugin_package_module.__file__
170 self.__base_class = base_plugin_class
171 self.__plugin_package_module = plugin_package_module
172 self.__default_plugin_name = default_plugin_name
173 self.__fallback_plugin_name = fallback_plugin_name
174 self.__egg_entry_group = egg_entry_point_group
175 self.__cache = None
176 self.__inspect_cache = None
177
179 """
180 Clear available plugins cache. When calling get_available_plugins()
181 module object is parsed again.
182 """
183 self.__cache = None
184 self.__inspect_cache = None
185
187 """
188 This method verifies if given object is a valid plugin.
189
190 @return: True, if valid
191 @rtype: bool
192 """
193
194 if self.__inspect_cache is None:
195 self.__inspect_cache = {}
196
197
198 obj_memory_pos = id(obj)
199
200
201
202 inspected_rc = self.__inspect_cache.get(obj_memory_pos)
203 if inspected_rc is not None:
204
205 return inspected_rc
206
207 base_api = self.__base_class.BASE_PLUGIN_API_VERSION
208
209 if not inspect.isclass(obj):
210 self.__inspect_cache[obj_memory_pos] = False
211 return False
212
213 if not issubclass(obj, self.__base_class):
214 self.__inspect_cache[obj_memory_pos] = False
215 return False
216
217 if hasattr(obj, '__subclasses__'):
218
219 if obj.__subclasses__():
220 self.__inspect_cache[obj_memory_pos] = False
221 return False
222 else:
223 sys.stderr.write("!!! Entropy Plugin warning: " \
224 "%s is not a new style class !!!\n" % (obj,))
225
226 if obj is self.__base_class:
227
228
229 self.__inspect_cache[obj_memory_pos] = False
230 return False
231
232 if not hasattr(obj, "PLUGIN_API_VERSION"):
233 sys.stderr.write("!!! Entropy Plugin warning: " \
234 "no PLUGIN_API_VERSION in %s !!!\n" % (obj,))
235 self.__inspect_cache[obj_memory_pos] = False
236 return False
237
238 if obj.PLUGIN_API_VERSION != base_api:
239 sys.stderr.write("!!! Entropy Plugin warning: " \
240 "PLUGIN_API_VERSION mismatch in %s !!!\n" % (obj,))
241 self.__inspect_cache[obj_memory_pos] = False
242 return False
243
244 if hasattr(obj, 'PLUGIN_DISABLED'):
245 if obj.PLUGIN_DISABLED:
246
247 self.__inspect_cache[obj_memory_pos] = False
248 return False
249
250 self.__inspect_cache[obj_memory_pos] = True
251 return True
252
254 """
255 Scan modules in given directory looking for a valid plugin class.
256 Directory is os.path.dirname(self.__modfile).
257
258 @return: module dictionary composed by module name as key and plugin
259 class as value
260 @rtype: dict
261 """
262 available = {}
263 pkg_modname = self.__plugin_package_module.__name__
264 mod_dir = os.path.dirname(self.__modfile)
265
266 for modname in os.listdir(mod_dir):
267
268 if modname.startswith("__"):
269 continue
270 if not (modname.endswith(EntropyPluginFactory._PYTHON_EXTENSION) \
271 or "." not in modname):
272 continue
273
274 if modname.endswith(EntropyPluginFactory._PYTHON_EXTENSION):
275 modname = modname[:-len(EntropyPluginFactory._PYTHON_EXTENSION)]
276
277 if not modname.endswith(EntropyPluginFactory._PLUGIN_SUFFIX):
278 continue
279
280
281 modname_clean = modname[:-len(EntropyPluginFactory._PLUGIN_SUFFIX)]
282
283 modpath = "%s.%s" % (pkg_modname, modname,)
284
285 try:
286 __import__(modpath)
287 except ImportError as err:
288 sys.stderr.write("!!! Entropy Plugin warning, cannot " \
289 "load module: %s | %s !!!\n" % (modpath, err,))
290 continue
291
292 for obj in list(sys.modules[modpath].__dict__.values()):
293
294 valid = self._inspect_object(obj)
295 if not valid:
296 continue
297
298 available[modname_clean] = obj
299
300 return available
301
303 """
304 Scan classes in given Python Egg group name looking for a valid plugin.
305
306 @return: module dictionary composed by module name as key and plugin
307 class as value
308 @rtype: dict
309 """
310
311 import pkg_resources
312 available = {}
313
314 for entry in pkg_resources.iter_entry_points(self.__egg_entry_group):
315
316 obj = entry.load()
317 valid = self._inspect_object(obj)
318 if not valid:
319 continue
320 available[entry.name] = obj
321
322 return available
323
324
326 """
327 Return currently available EntropyPlugin plugin classes.
328 Note: Entropy plugins can either be Python packages or modules and
329 their name MUST end with PluginFactory._PLUGIN_SUFFIX ("_plugin").
330
331 @return: dictionary composed by Entropy plugin id as key and Entropy
332 Python module as value
333 @rtype: dict
334 """
335 if self.__cache is not None:
336 return self.__cache.copy()
337
338 if self.__egg_entry_group:
339 available = self._scan_egg_group()
340 else:
341 available = self._scan_dir()
342
343 self.__cache = available.copy()
344 return available
345
347 """
348 Return currently configured Entropy Plugin class.
349
350 @return: Entropy plugin class
351 @rtype: entropy.core.EntropyPlugin
352 @raise KeyError: if default plugin is not set or not found
353 """
354 available = self.get_available_plugins()
355 plugin = self.__default_plugin_name
356 fallback = self.__fallback_plugin_name
357 klass = available.get(plugin)
358
359 if klass is None:
360 import warnings
361 warnings.warn("%s: %s" % (
362 "selected Plugin not available, using fallback", plugin,))
363 klass = available.get(fallback)
364
365 if klass is None:
366 raise KeyError
367
368 return klass
369