Back to index

obnam  1.1
pluginmgr.py
Go to the documentation of this file.
00001 # Copyright (C) 2009  Lars Wirzenius <liw@liw.fi>
00002 # 
00003 # This program is free software: you can redistribute it and/or modify
00004 # it under the terms of the GNU General Public License as published by
00005 # the Free Software Foundation, either version 3 of the License, or
00006 # (at your option) any later version.
00007 # 
00008 # This program is distributed in the hope that it will be useful,
00009 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00010 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00011 # GNU General Public License for more details.
00012 # 
00013 # You should have received a copy of the GNU General Public License
00014 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
00015 
00016 
00017 '''A generic plugin manager.
00018 
00019 The plugin manager finds files with plugins and loads them. It looks
00020 for plugins in a number of locations specified by the caller. To add
00021 a plugin to be loaded, it is enough to put it in one of the locations,
00022 and name it *_plugin.py. (The naming convention is to allow having
00023 other modules as well, such as unit tests, in the same locations.)
00024 
00025 '''
00026 
00027 
00028 import imp
00029 import inspect
00030 import os
00031 
00032 
00033 class Plugin(object):
00034 
00035     '''Base class for plugins.
00036     
00037     A plugin MUST NOT have any side effects when it is instantiated.
00038     This is necessary so that it can be safely loaded by unit tests,
00039     and so that a user interface can allow the user to disable it,
00040     even if it is installed, with no ill effects. Any side effects
00041     that would normally happen should occur in the enable() method,
00042     and be undone by the disable() method. These methods must be
00043     callable any number of times.
00044     
00045     The subclass MAY define the following attributes:
00046     
00047     * name
00048     * description
00049     * version
00050     * required_application_version
00051     
00052     name is the user-visible identifier for the plugin. It defaults
00053     to the plugin's classname.
00054     
00055     description is the user-visible description of the plugin. It may
00056     be arbitrarily long, and can use pango markup language. Defaults
00057     to the empty string.
00058     
00059     version is the plugin version. Defaults to '0.0.0'. It MUST be a
00060     sequence of integers separated by periods. If several plugins with
00061     the same name are found, the newest version is used. Versions are
00062     compared integer by integer, starting with the first one, and a 
00063     missing integer treated as a zero. If two plugins have the same 
00064     version, either might be used.
00065     
00066     required_application_version gives the version of the minimal 
00067     application version the plugin is written for. The first integer
00068     must match exactly: if the application is version 2.3.4, the
00069     plugin's required_application_version must be at least 2 and
00070     at most 2.3.4 to be loaded. Defaults to 0.
00071     
00072     '''
00073     
00074     @property
00075     def name(self):
00076         return self.__class__.__name__
00077         
00078     @property
00079     def description(self):
00080         return ''
00081         
00082     @property
00083     def version(self):
00084         return '0.0.0'
00085         
00086     @property
00087     def required_application_version(self):
00088         return '0.0.0'
00089         
00090     def enable_wrapper(self):
00091         '''Enable plugin.
00092         
00093         The plugin manager will call this method, which then calls the
00094         enable method. Plugins should implement the enable method.
00095         The wrapper method is there to allow an application to provide
00096         an extended base class that does some application specific
00097         magic when plugins are enabled or disabled.
00098         
00099         '''
00100         
00101         self.enable()
00102 
00103     def disable_wrapper(self):
00104         '''Corresponds to enable_wrapper, but for disabling a plugin.'''
00105         self.disable()
00106     
00107     def enable(self):
00108         '''Enable the plugin.'''
00109         raise NotImplemented()
00110     
00111     def disable(self):
00112         '''Disable the plugin.'''
00113         raise NotImplemented()
00114 
00115 
00116 class PluginManager(object):
00117 
00118     '''Manage plugins.
00119     
00120     This class finds and loads plugins, and keeps a list of them that
00121     can be accessed in various ways.
00122     
00123     The locations are set via the locations attribute, which is a list.
00124     
00125     When a plugin is loaded, an instance of its class is created. This
00126     instance is initialized using normal and keyword arguments specified
00127     in the plugin manager attributes plugin_arguments and 
00128     plugin_keyword_arguments.
00129     
00130     The version of the application using the plugin manager is set via
00131     the application_version attribute. This defaults to '0.0.0'.
00132     
00133     '''
00134     
00135     suffix = '_plugin.py'
00136 
00137     def __init__(self):
00138         self.locations = []
00139         self._plugins = None
00140         self._plugin_files = None
00141         self.plugin_arguments = []
00142         self.plugin_keyword_arguments = {}
00143         self.application_version = '0.0.0'
00144 
00145     @property
00146     def plugin_files(self):
00147         if self._plugin_files is None:
00148             self._plugin_files = self.find_plugin_files()
00149         return self._plugin_files
00150 
00151     @property
00152     def plugins(self):
00153         if self._plugins is None:
00154             self._plugins = self.load_plugins()
00155         return self._plugins
00156 
00157     def __getitem__(self, name):
00158         for plugin in self.plugins:
00159             if plugin.name == name:
00160                 return plugin
00161         raise KeyError('Plugin %s is not known' % name)
00162 
00163     def find_plugin_files(self):
00164         '''Find files that may contain plugins.
00165         
00166         This finds all files named *_plugin.py in all locations.
00167         The returned list is sorted.
00168         
00169         '''
00170         
00171         pathnames = []
00172         
00173         for location in self.locations:
00174             try:
00175                 basenames = os.listdir(location)
00176             except os.error:
00177                 continue
00178             for basename in basenames:
00179                 s = os.path.join(location, basename)
00180                 if s.endswith(self.suffix) and os.path.exists(s):
00181                     pathnames.append(s)
00182         
00183         return sorted(pathnames)
00184 
00185     def load_plugins(self):
00186         '''Load plugins from all plugin files.'''
00187         
00188         plugins = dict()
00189         
00190         for pathname in self.plugin_files:
00191             for plugin in self.load_plugin_file(pathname):
00192                 if plugin.name in plugins:
00193                     p = plugins[plugin.name]
00194                     if self.is_older(p.version, plugin.version):
00195                         plugins[plugin.name] = plugin
00196                 else:
00197                     plugins[plugin.name] = plugin
00198 
00199         return plugins.values()
00200 
00201     def is_older(self, version1, version2):
00202         '''Is version1 older than version2?'''
00203         return self.parse_version(version1) < self.parse_version(version2)
00204 
00205     def load_plugin_file(self, pathname):
00206         '''Return plugin classes in a plugin file.'''
00207 
00208         name, ext = os.path.splitext(os.path.basename(pathname))
00209         f = file(pathname, 'r')
00210         module = imp.load_module(name, f, pathname, 
00211                                  ('.py', 'r', imp.PY_SOURCE))
00212         f.close()
00213         
00214         plugins = []
00215         for dummy, member in inspect.getmembers(module, inspect.isclass):
00216             if issubclass(member, Plugin):
00217                 p = member(*self.plugin_arguments,
00218                            **self.plugin_keyword_arguments)
00219                 if self.compatible_version(p.required_application_version):
00220                     plugins.append(p)
00221         
00222         return plugins
00223 
00224     def compatible_version(self, required_application_version):
00225         '''Check that the plugin is version-compatible with the application.
00226         
00227         This checks the plugin's required_application_version against
00228         the declared application version and returns True if they are
00229         compatible, and False if not.
00230         
00231         '''
00232 
00233         req = self.parse_version(required_application_version)
00234         app = self.parse_version(self.application_version)
00235         
00236         return app[0] == req[0] and app >= req
00237 
00238     def parse_version(self, version):
00239         '''Parse a string represenation of a version into list of ints.'''
00240         
00241         return [int(s) for s in version.split('.')]
00242 
00243     def enable_plugins(self, plugins=None):
00244         '''Enable all or selected plugins.'''
00245         
00246         for plugin in plugins or self.plugins:
00247             plugin.enable_wrapper()
00248 
00249     def disable_plugins(self, plugins=None):
00250         '''Disable all or selected plugins.'''
00251         
00252         for plugin in plugins or self.plugins:
00253             plugin.disable_wrapper()
00254