321 lines
11 KiB
Python
321 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Molecule Disc Image builder for Sabayon Linux
|
|
# Copyright (C) 2009 Fabio Erculiani
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
import sys
|
|
import os
|
|
import inspect
|
|
|
|
class PluginFactory:
|
|
|
|
"""
|
|
Generic Molecule Components Plugin Factory (loader).
|
|
"""
|
|
|
|
_PLUGIN_SUFFIX = "_plugin"
|
|
_PYTHON_EXTENSION = ".py"
|
|
# A colon separated list of modules to load, looking for
|
|
# Molecule plugins.
|
|
_PLUGIN_MODULES = os.getenv("MOLECULE_PLUGIN_MODULES")
|
|
|
|
def __init__(self, base_plugin_class, plugin_package_module,
|
|
default_plugin_name = None, fallback_plugin_name = None,
|
|
egg_entry_point_group = None):
|
|
"""
|
|
Molecule 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
|
|
Molecule 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("!!! Molecule 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("!!! Molecule 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("!!! Molecule 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)
|
|
modules = []
|
|
|
|
for modname in os.listdir(mod_dir):
|
|
|
|
if modname.startswith("__"):
|
|
continue # python stuff
|
|
if not (modname.endswith(PluginFactory._PYTHON_EXTENSION) \
|
|
or "." not in modname):
|
|
continue # not something we want to load
|
|
|
|
if modname.endswith(PluginFactory._PYTHON_EXTENSION):
|
|
modname = modname[:-len(PluginFactory._PYTHON_EXTENSION)]
|
|
|
|
if not modname.endswith(PluginFactory._PLUGIN_SUFFIX):
|
|
continue
|
|
|
|
# remove suffix
|
|
modname_clean = modname[:-len(PluginFactory._PLUGIN_SUFFIX)]
|
|
|
|
modpath = "%s.%s" % (pkg_modname, modname,)
|
|
modules.append(modpath)
|
|
|
|
plugin_modules = self._PLUGIN_MODULES
|
|
if plugin_modules:
|
|
modules.extend(plugin_modules.split(":"))
|
|
|
|
for modpath in modules:
|
|
|
|
try:
|
|
__import__(modpath)
|
|
except ImportError as err:
|
|
sys.stderr.write("!!! Molecule 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[modpath] = 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: Molecule plugins can either be Python packages or modules and
|
|
their name MUST end with PluginFactory._PLUGIN_SUFFIX ("_plugin").
|
|
|
|
@return: dictionary composed by Molecule plugin id as key and Molecule
|
|
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 Molecule Plugin class.
|
|
|
|
@return: Molecule plugin class
|
|
@rtype: default plugin class object
|
|
@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
|
|
|
|
_SPEC_FACTORY = None
|
|
|
|
@staticmethod
|
|
def _get_spec():
|
|
if PluginFactory._SPEC_FACTORY is None:
|
|
from molecule.specs.skel import GenericSpec
|
|
try:
|
|
from .. import plugins as plugs
|
|
except ImportError:
|
|
from . import plugins as plugs
|
|
PluginFactory._SPEC_FACTORY = PluginFactory(GenericSpec, plugs)
|
|
return PluginFactory._SPEC_FACTORY
|
|
|
|
@staticmethod
|
|
def get_spec_plugins():
|
|
factory = PluginFactory._get_spec()
|
|
plugins = factory.get_available_plugins()
|
|
spec_plugins = dict((y.execution_strategy(), y) for x, y \
|
|
in list(plugins.items()))
|
|
return spec_plugins
|