[molecule] implement real Molecule Plugin Factory and use it
This commit is contained in:
+13
-8
@@ -18,8 +18,6 @@
|
||||
|
||||
import os
|
||||
from molecule.exception import SpecFileError
|
||||
from molecule.specs.plugins import SPEC_PLUGS
|
||||
from molecule.specs.plugins.builtin import LivecdSpec
|
||||
from molecule.specs.skel import GenericSpec
|
||||
from molecule.version import VERSION
|
||||
import molecule.utils
|
||||
@@ -82,18 +80,24 @@ class Configuration(dict):
|
||||
|
||||
class SpecParser:
|
||||
|
||||
# FIXME: kept for backward .spec files compatibility where
|
||||
# execution_strategy argument is not set
|
||||
DEFAULT_PARSER = LivecdSpec
|
||||
|
||||
def __init__(self, filepath):
|
||||
|
||||
self.filepath = filepath[:]
|
||||
|
||||
# FIXME: kept for backward .spec files compatibility where
|
||||
# execution_strategy argument is not set
|
||||
# TODO: remove and make execution_strategy mandatory in .spec file
|
||||
from molecule.specs.plugins.builtin_plugin import LivecdSpec
|
||||
|
||||
execution_strategy = self.parse_execution_strategy()
|
||||
if execution_strategy is None:
|
||||
execution_strategy = SpecParser.DEFAULT_PARSER.execution_strategy()
|
||||
execution_strategy = LivecdSpec.execution_strategy()
|
||||
|
||||
plugin = SPEC_PLUGS.get(execution_strategy)
|
||||
from molecule.specs.factory import PluginFactory
|
||||
spec_plugins = PluginFactory.get_spec_plugins()
|
||||
print spec_plugins
|
||||
|
||||
plugin = spec_plugins.get(execution_strategy)
|
||||
if plugin is None:
|
||||
raise SpecFileError("Execution strategy provided in %s spec file"
|
||||
" not supported, strategy: %s" % (
|
||||
@@ -132,6 +136,7 @@ class SpecParser:
|
||||
# compact lines properly
|
||||
old_key = None
|
||||
for line in data:
|
||||
key = None
|
||||
if ":" in line:
|
||||
key, value = self.parse_line_statement(line)
|
||||
if key is None:
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
# -*- 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"
|
||||
|
||||
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)
|
||||
|
||||
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,)
|
||||
|
||||
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[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: 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
|
||||
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 plugins.items())
|
||||
return spec_plugins
|
||||
@@ -15,11 +15,3 @@
|
||||
# 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.
|
||||
|
||||
from molecule.specs.plugins.builtin import LivecdSpec
|
||||
from molecule.specs.plugins.remaster import RemasterSpec
|
||||
from molecule.specs.plugins.tar import IsoToTarSpec
|
||||
|
||||
# FIXME: this will need to be pluggable (and plugin factory is required)
|
||||
SPEC_PLUGS = dict((x.execution_strategy(), x,) for x in \
|
||||
(LivecdSpec, RemasterSpec, IsoToTarSpec))
|
||||
|
||||
@@ -589,6 +589,8 @@ class IsoHandler(GenericExecutionStep, BuiltinHandlerMixin):
|
||||
|
||||
class LivecdSpec(GenericSpec):
|
||||
|
||||
PLUGIN_API_VERSION = 0
|
||||
|
||||
@staticmethod
|
||||
def execution_strategy():
|
||||
return "livecd"
|
||||
@@ -24,10 +24,13 @@ from molecule.i18n import _
|
||||
from molecule.output import red, brown, blue, green, purple, darkgreen, \
|
||||
darkred, bold, darkblue, readtext
|
||||
from molecule.specs.skel import GenericExecutionStep, GenericSpec
|
||||
from molecule.specs.plugins.builtin import ChrootHandler as BuiltinChrootHandler
|
||||
from molecule.specs.plugins.builtin import CdrootHandler as BuiltinCdrootHandler
|
||||
from molecule.specs.plugins.builtin import IsoHandler as BuiltinIsoHandler
|
||||
from molecule.specs.plugins.builtin import BuiltinHandlerMixin
|
||||
from molecule.specs.plugins.builtin_plugin import ChrootHandler as \
|
||||
BuiltinChrootHandler
|
||||
from molecule.specs.plugins.builtin_plugin import CdrootHandler as \
|
||||
BuiltinCdrootHandler
|
||||
from molecule.specs.plugins.builtin_plugin import IsoHandler as \
|
||||
BuiltinIsoHandler
|
||||
from molecule.specs.plugins.builtin_plugin import BuiltinHandlerMixin
|
||||
import molecule.utils
|
||||
|
||||
class IsoUnpackHandler(GenericExecutionStep, BuiltinHandlerMixin):
|
||||
@@ -308,6 +311,8 @@ class IsoHandler(BuiltinIsoHandler):
|
||||
|
||||
class RemasterSpec(GenericSpec):
|
||||
|
||||
PLUGIN_API_VERSION = 0
|
||||
|
||||
@staticmethod
|
||||
def execution_strategy():
|
||||
return "iso_remaster"
|
||||
@@ -23,8 +23,8 @@ import shutil
|
||||
from molecule.i18n import _
|
||||
from molecule.output import blue, darkred
|
||||
from molecule.specs.skel import GenericExecutionStep, GenericSpec
|
||||
from molecule.specs.plugins.builtin import BuiltinHandlerMixin
|
||||
from molecule.specs.plugins.remaster import IsoUnpackHandler, \
|
||||
from molecule.specs.plugins.builtin_plugin import BuiltinHandlerMixin
|
||||
from molecule.specs.plugins.remaster_plugin import IsoUnpackHandler, \
|
||||
ChrootHandler
|
||||
import molecule.utils
|
||||
|
||||
@@ -108,6 +108,8 @@ class TarHandler(GenericExecutionStep, BuiltinHandlerMixin):
|
||||
|
||||
class IsoToTarSpec(GenericSpec):
|
||||
|
||||
PLUGIN_API_VERSION = 0
|
||||
|
||||
@staticmethod
|
||||
def execution_strategy():
|
||||
return "iso_to_tar"
|
||||
@@ -142,10 +142,13 @@ class GenericExecutionStep:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class GenericSpec(GenericSpecFunctions):
|
||||
class GenericSpec(object, GenericSpecFunctions):
|
||||
|
||||
EXECUTION_STRATEGY_KEY = "execution_strategy"
|
||||
|
||||
# Molecule Plugin factory support
|
||||
BASE_PLUGIN_API_VERSION = 0
|
||||
|
||||
@staticmethod
|
||||
def execution_strategy():
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user