386 lines
13 KiB
Python
386 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
|
|
@author: Fabio Erculiani <lxnay@sabayon.org>
|
|
@contact: lxnay@sabayon.org
|
|
@copyright: Fabio Erculiani
|
|
@license: GPL-2
|
|
|
|
B{Entropy Framework core module}.
|
|
|
|
This module contains base classes used by entropy.client,
|
|
entropy.server and entropy.services.
|
|
|
|
"Singleton" is a class that is inherited from singleton objects.
|
|
|
|
"""
|
|
import sys
|
|
import os
|
|
import inspect
|
|
from entropy.const import etpConst
|
|
|
|
class Singleton(object):
|
|
|
|
"""
|
|
If your class wants to become a sexy Singleton,
|
|
subclass this and replace __init__ with init_singleton.
|
|
Your subclass can expose a method called "is_destroyed()" that
|
|
returns a bool stating if singleton instance has been destroyed.
|
|
"""
|
|
|
|
def __new__(cls, *args, **kwds):
|
|
|
|
# Support for overridable singleton class used for loading
|
|
# instance. This is required when your Singleton class is subclassed
|
|
# and reloaded using different class stack, just push the preferred
|
|
# singleton class into Class.__singleton_class__
|
|
cls = getattr(cls, '__singleton_class__', cls)
|
|
|
|
singleton = getattr(cls, '__singleton__', None)
|
|
if singleton is not None:
|
|
destroyed = getattr(singleton, 'is_destroyed', None)
|
|
if destroyed is not None:
|
|
if not destroyed():
|
|
return singleton
|
|
cls.__singleton__ = None
|
|
else:
|
|
return singleton
|
|
|
|
# dict support
|
|
if issubclass(cls, dict):
|
|
singleton = dict.__new__(cls)
|
|
else:
|
|
singleton = object.__new__(cls)
|
|
singleton.init_singleton(*args, **kwds)
|
|
cls.__singleton__ = singleton
|
|
return singleton
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""
|
|
This is a fake method, necessary for Python 3.
|
|
"""
|
|
pass
|
|
|
|
class EntropyPluginStore(object):
|
|
|
|
"""
|
|
This is a base class for handling a map of plugin objects by providing
|
|
generic add/remove functions.
|
|
"""
|
|
|
|
def __init__(self):
|
|
object.__init__(self)
|
|
self.__plugins = {}
|
|
|
|
def get_plugins(self):
|
|
"""
|
|
Return a copy of the internal dictionary that contains the currently
|
|
stored plugins map.
|
|
|
|
@return: stored plugins map
|
|
@rtype: dict
|
|
"""
|
|
return self.__plugins.copy()
|
|
|
|
def drop_plugins(self):
|
|
"""
|
|
Drop all the currently stored plugins from storage.
|
|
"""
|
|
self.__plugins.clear()
|
|
|
|
def add_plugin(self, plugin_id, plugin_object):
|
|
"""
|
|
This method lets you add a plugin to the store.
|
|
|
|
@param plugin_id: plugin identifier
|
|
@type plugin_id: string
|
|
@param plugin_object: valid SystemSettingsPlugin instance
|
|
@type plugin_object: any Python object
|
|
"""
|
|
self.__plugins[plugin_id] = plugin_object
|
|
|
|
def remove_plugin(self, plugin_id):
|
|
"""
|
|
This method lets you remove previously added plugin objects via
|
|
identifiers.
|
|
|
|
@param plugin_id: plugin identifier
|
|
@type plugin_id: basestring
|
|
"""
|
|
del self.__plugins[plugin_id]
|
|
|
|
def has_plugin(self, plugin_id):
|
|
"""
|
|
Return whether EntropyPluginStore instance contains the given
|
|
plugin id.
|
|
"""
|
|
return plugin_id in self.__plugins
|
|
|
|
class EntropyPluginFactory:
|
|
|
|
"""
|
|
Generic Entropy Components Plugin Factory (loader).
|
|
"""
|
|
|
|
_PLUGIN_SUFFIX = "_plugin"
|
|
_PYTHON_EXTENSION = ".py"
|
|
|
|
def __init__(self, base_plugin_class, plugin_package_module,
|
|
default_plugin_name = None, fallback_plugin_name = None,
|
|
egg_entry_point_group = None):
|
|
"""
|
|
Entropy Generic Plugin Factory constructor.
|
|
MANDATORY: every plugin module/package(name) must end with _plugin
|
|
suffix.
|
|
|
|
Base plugin classes must have the following class attributes set:
|
|
|
|
- BASE_PLUGIN_API_VERSION: integer describing API revision in use
|
|
in class
|
|
|
|
Subclasses of Base plugin class must have the following class
|
|
attributes set:
|
|
|
|
- PLUGIN_API_VERSION: integer describing the currently implemented
|
|
plugin API revision, must match with BASE_PLUGIN_API_VERSION
|
|
above otherwise plugin won't be loaded and a warning will be
|
|
printed.
|
|
|
|
Moreover, plugin classes must be "Python new-style classes", otherwise
|
|
parser won't be able to determine if classes have subclasses and thus
|
|
pick the proper object (one with no subclasses!!).
|
|
See: http://www.python.org/doc/newstyle -- in other words, you have
|
|
to inherit the built-in "object" class (yeah, it's called object).
|
|
So, even if using normal classes could work, if you start doing nasty
|
|
things (nested inherittance of plugin classes), behaviour cannot
|
|
be guaranteed.
|
|
If it's not clear, let me repeat once again, valid plugin classes
|
|
must not have subclasses around! Think about it, it's an obvious thing.
|
|
|
|
If plugin class features a "PLUGIN_DISABLED" class attribute with
|
|
a boolean value of True, such plugin will be ignored.
|
|
|
|
If egg_entry_point_group is specified, Python Egg support is enabled
|
|
and classes are loaded via this infrastructure.
|
|
NOTE: if egg_entry_point_group is set, you NEED the setuptools package.
|
|
|
|
@param base_plugin_class: Base class that valid plugin classes must
|
|
inherit from.
|
|
@type base_plugin_class: class
|
|
@param plugin_package_module: every plugin repository must work as
|
|
Python package, the value of this argument must be a valid
|
|
Python package module that can be scanned looking for valid
|
|
Entropy Plugin classes.
|
|
@type plugin_package_module: Python module
|
|
@keyword default_plugin_name: identifier of the default plugin to load
|
|
@type default_plugin_name: string
|
|
@keyword fallback_plugin_name: identifier of the fallback plugin to load
|
|
if default is not available
|
|
@type fallback_plugin_name: string
|
|
@keyword egg_entry_point_group: valid Python Egg entry point group, in
|
|
this case, Python Egg support is used
|
|
@type egg_entry_point_group: string
|
|
@raise AttributeError: when passed plugin_package_module is not a
|
|
valid Python package module
|
|
"""
|
|
self.__modfile = plugin_package_module.__file__
|
|
self.__base_class = base_plugin_class
|
|
self.__plugin_package_module = plugin_package_module
|
|
self.__default_plugin_name = default_plugin_name
|
|
self.__fallback_plugin_name = fallback_plugin_name
|
|
self.__egg_entry_group = egg_entry_point_group
|
|
self.__cache = None
|
|
self.__inspect_cache = None
|
|
|
|
def clear_cache(self):
|
|
"""
|
|
Clear available plugins cache. When calling get_available_plugins()
|
|
module object is parsed again.
|
|
"""
|
|
self.__cache = None
|
|
self.__inspect_cache = None
|
|
|
|
def _inspect_object(self, obj):
|
|
"""
|
|
This method verifies if given object is a valid plugin.
|
|
|
|
@return: True, if valid
|
|
@rtype: bool
|
|
"""
|
|
|
|
if self.__inspect_cache is None:
|
|
self.__inspect_cache = {}
|
|
|
|
# use memory position
|
|
obj_memory_pos = id(obj)
|
|
# To avoid infinite recursion and improve
|
|
# objects inspection, check if obj has been already
|
|
# analyzed
|
|
inspected_rc = self.__inspect_cache.get(obj_memory_pos)
|
|
if inspected_rc is not None:
|
|
# avoid infinite recursion
|
|
return inspected_rc
|
|
|
|
base_api = self.__base_class.BASE_PLUGIN_API_VERSION
|
|
|
|
if not inspect.isclass(obj):
|
|
self.__inspect_cache[obj_memory_pos] = False
|
|
return False
|
|
|
|
if not issubclass(obj, self.__base_class):
|
|
self.__inspect_cache[obj_memory_pos] = False
|
|
return False
|
|
|
|
if hasattr(obj, '__subclasses__'):
|
|
# new style class
|
|
if obj.__subclasses__(): # only lower classes taken
|
|
self.__inspect_cache[obj_memory_pos] = False
|
|
return False
|
|
else:
|
|
sys.stderr.write("!!! Entropy Plugin warning: " \
|
|
"%s is not a new style class !!!\n" % (obj,))
|
|
|
|
if obj is self.__base_class:
|
|
# in this case, obj is our base class,
|
|
# so we are very sure that obj is not valid
|
|
self.__inspect_cache[obj_memory_pos] = False
|
|
return False
|
|
|
|
if not hasattr(obj, "PLUGIN_API_VERSION"):
|
|
sys.stderr.write("!!! Entropy Plugin warning: " \
|
|
"no PLUGIN_API_VERSION in %s !!!\n" % (obj,))
|
|
self.__inspect_cache[obj_memory_pos] = False
|
|
return False
|
|
|
|
if obj.PLUGIN_API_VERSION != base_api:
|
|
sys.stderr.write("!!! Entropy Plugin warning: " \
|
|
"PLUGIN_API_VERSION mismatch in %s !!!\n" % (obj,))
|
|
self.__inspect_cache[obj_memory_pos] = False
|
|
return False
|
|
|
|
if hasattr(obj, 'PLUGIN_DISABLED'):
|
|
if obj.PLUGIN_DISABLED:
|
|
# this plugin has been disabled
|
|
self.__inspect_cache[obj_memory_pos] = False
|
|
return False
|
|
|
|
self.__inspect_cache[obj_memory_pos] = True
|
|
return True
|
|
|
|
def _scan_dir(self):
|
|
"""
|
|
Scan modules in given directory looking for a valid plugin class.
|
|
Directory is os.path.dirname(self.__modfile).
|
|
|
|
@return: module dictionary composed by module name as key and plugin
|
|
class as value
|
|
@rtype: dict
|
|
"""
|
|
available = {}
|
|
pkg_modname = self.__plugin_package_module.__name__
|
|
mod_dir = os.path.dirname(self.__modfile)
|
|
|
|
for modname in os.listdir(mod_dir):
|
|
|
|
if modname.startswith("__"):
|
|
continue # python stuff
|
|
if not (modname.endswith(EntropyPluginFactory._PYTHON_EXTENSION) \
|
|
or "." not in modname):
|
|
continue # not something we want to load
|
|
|
|
if modname.endswith(EntropyPluginFactory._PYTHON_EXTENSION):
|
|
modname = modname[:-len(EntropyPluginFactory._PYTHON_EXTENSION)]
|
|
|
|
if not modname.endswith(EntropyPluginFactory._PLUGIN_SUFFIX):
|
|
continue
|
|
|
|
# remove suffix
|
|
modname_clean = modname[:-len(EntropyPluginFactory._PLUGIN_SUFFIX)]
|
|
|
|
modpath = "%s.%s" % (pkg_modname, modname,)
|
|
|
|
try:
|
|
__import__(modpath)
|
|
except ImportError as err:
|
|
sys.stderr.write("!!! Entropy Plugin warning, cannot " \
|
|
"load module: %s | %s !!!\n" % (modpath, err,))
|
|
continue
|
|
|
|
for obj in list(sys.modules[modpath].__dict__.values()):
|
|
|
|
valid = self._inspect_object(obj)
|
|
if not valid:
|
|
continue
|
|
|
|
available[modname_clean] = obj
|
|
|
|
return available
|
|
|
|
def _scan_egg_group(self):
|
|
"""
|
|
Scan classes in given Python Egg group name looking for a valid plugin.
|
|
|
|
@return: module dictionary composed by module name as key and plugin
|
|
class as value
|
|
@rtype: dict
|
|
"""
|
|
# needs setuptools
|
|
import pkg_resources
|
|
available = {}
|
|
|
|
for entry in pkg_resources.iter_entry_points(self.__egg_entry_group):
|
|
|
|
obj = entry.load()
|
|
valid = self._inspect_object(obj)
|
|
if not valid:
|
|
continue
|
|
available[entry.name] = obj
|
|
|
|
return available
|
|
|
|
|
|
def get_available_plugins(self):
|
|
"""
|
|
Return currently available plugin classes.
|
|
Note: Entropy plugins can either be Python packages or modules and
|
|
their name MUST end with PluginFactory._PLUGIN_SUFFIX ("_plugin").
|
|
|
|
@return: dictionary composed by Entropy plugin id as key and Entropy
|
|
Python module as value
|
|
@rtype: dict
|
|
"""
|
|
if self.__cache is not None:
|
|
return self.__cache.copy()
|
|
|
|
if self.__egg_entry_group:
|
|
available = self._scan_egg_group()
|
|
else:
|
|
available = self._scan_dir()
|
|
|
|
self.__cache = available.copy()
|
|
return available
|
|
|
|
def get_default_plugin(self):
|
|
"""
|
|
Return currently configured Entropy Plugin class.
|
|
|
|
@return: Entropy plugin class
|
|
@rtype: default plugin class given
|
|
@raise KeyError: if default plugin is not set or not found
|
|
"""
|
|
available = self.get_available_plugins()
|
|
plugin = self.__default_plugin_name
|
|
fallback = self.__fallback_plugin_name
|
|
klass = available.get(plugin)
|
|
|
|
if klass is None:
|
|
import warnings
|
|
warnings.warn("%s: %s" % (
|
|
"selected Plugin not available, using fallback", plugin,))
|
|
klass = available.get(fallback)
|
|
|
|
if klass is None:
|
|
raise KeyError
|
|
|
|
return klass
|