Files

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