IGN:Framework to support 3rd party plugins

This commit is contained in:
Kovid Goyal 2008-12-21 21:31:57 -08:00
parent 30e98dc705
commit 61e0f3db20
6 changed files with 338 additions and 4 deletions

View File

@ -0,0 +1,69 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
class Plugin(object):
#: List of platforms this plugin works on
#: For example: ``['windows', 'osx', 'linux']
supported_platforms = []
#: The name of this plugin
name = 'Trivial Plugin'
#: The version of this plugin as a 3-tuple (major, minor, revision)
version = (1, 0, 0)
#: A short string describing what this plugin does
description = _('Does absolutely nothing')
#: The author of this plugin
author = _('Unknown')
#: When more than one plugin exists for a filetype,
#: the plugins are run in order of decreasing priority
#: i.e. plugins with higher priority will be run first.
#: The highest possible priority is ``sys.maxint``.
#: Default pririty is 1.
priority = 1
#: The earliest version of calibre this plugin requires
minimum_calibre_version = (0, 4, 118)
def __init__(self, plugin_path):
'''
Called once when calibre plugins are initialized. Plugins are re-initialized
every time a new plugin is added.
:param plugin_path: Path to the zip file this plugin is distributed in.
'''
self.plugin_path = plugin_path
def customization_help(self):
'''
Return a string giving help on how to customize this plugin.
By default raise a :class:`NotImplementedError`, which indicates that
the plugin does not require customization.
'''
raise NotImplementedError
def run(self, path_to_ebook, site_customization=''):
'''
Run the plugin. Must be implemented in subclasses.
It should perform whatever modifications are required
on the ebook and return the absolute path to the
modified ebook. If no modifications are needed, it should
return the path to the original ebook. If an error is encountered
it should raise an Exception. The default implementation
simply return the path to the original ebook.
:param path_to_ebook: Absolute path to the ebook
:param site_customization: A (possibly empty) string that the user
has specified to customize this plugin.
For example, it could be the path to a needed
executable on her system.
:return: Absolute path to the modified ebook.
'''
# Default implementation does nothing
return path_to_ebook

View File

@ -0,0 +1,28 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from calibre.customize import Plugin as PluginBase
class Plugin(PluginBase):
'''
A plugin that is associated with a particular set of file types.
'''
#: List of file types for which this plugin should be run
#: For example: ``['lit', 'mobi', 'prc']``
file_types = []
#: If True, this plugin is run when books are added
#: to the database
on_import = False
#: If True, this plugin is run whenever an any2* tool
#: is used, on the file passed to the any2* tool.
on_preprocess = False
#: If True, this plugin is run after an any2* tool is
#: used, on the final file produced by the tool.
on_postprocess = False

228
src/calibre/customize/ui.py Normal file
View File

@ -0,0 +1,228 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, shutil, traceback, functools, sys
from calibre.customize import Plugin
from calibre.customize.filetype import Plugin as FileTypePlugin
from calibre.constants import __version__, iswindows, isosx
from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
plugin_dir, OptionParser
version = tuple([int(x) for x in __version__.split('.')])
platform = 'linux'
if iswindows:
platform = 'windows'
if isosx:
platform = 'osx'
from zipfile import ZipFile
def _config():
c = Config('customize')
c.add_opt('plugins', default={}, help=_('Installed plugins'))
c.add_opt('filetype_mapping', default={}, help=_('Maping for filetype plugins'))
c.add_opt('plugin_customization', default={}, help=_('Local plugin customization'))
c.add_opt('disabled_plugins', default=set([]), help=_('Disabled plugins'))
return ConfigProxy(c)
config = _config()
class InvalidPlugin(ValueError):
pass
def load_plugin(path_to_zip_file):
'''
Load plugin from zip file or raise InvalidPlugin error
:return: A :class:`Plugin` instance.
'''
print 'Loading plugin from', path_to_zip_file
zf = ZipFile(path_to_zip_file)
for name in zf.namelist():
if name.lower().endswith('plugin.py'):
locals = {}
exec zf.read(name) in locals
for x in locals.values():
if isinstance(x, type) and issubclass(x, Plugin):
if x.minimum_calibre_version > version:
raise InvalidPlugin(_('%s needs calibre version at least %s')%
(x.name, x.minimum_calibre_version))
if platform not in x.supported_platforms:
raise InvalidPlugin(_('%s is not supported on %s')%
(x.name, platform))
return x
raise InvalidPlugin(_('No valid plugin found in ')+path_to_zip_file)
_initialized_plugins = []
_on_import = {}
_on_preprocess = {}
_on_postprocess = {}
def reread_filetype_plugins():
global _on_import
global _on_preprocess
global _on_postprocess
_on_import = {}
_on_preprocess = {}
_on_postprocess = {}
for plugin in _initialized_plugins:
if isinstance(plugin, FileTypePlugin):
for ft in plugin.file_types:
if plugin.on_import:
if not _on_import.has_key(ft):
_on_import[ft] = []
_on_import[ft].append(plugin)
if plugin.on_preprocess:
if not _on_preprocess.has_key(ft):
_on_preprocess[ft] = []
_on_preprocess[ft].append(plugin)
if plugin.on_postprocess:
if not _on_postprocess.has_key(ft):
_on_postprocess[ft] = []
_on_postprocess[ft].append(plugin)
def _run_filetype_plugins(path_to_file, ft, occasion='preprocess'):
occasion = {'import':_on_import, 'preprocess':_on_preprocess,
'postprocess':_on_postprocess}[occasion]
customization = config['plugin_customization']
nfp = path_to_file
for plugin in occasion.get(ft, []):
if is_disabled(plugin):
continue
sc = customization.get(plugin.name, '')
try:
nfp = plugin.run(path_to_file, sc)
except:
print 'Running file type plugin %s failed with traceback:'%plugin.name
traceback.print_exc()
return nfp
run_plugins_on_import = functools.partial(_run_filetype_plugins,
occasion='import')
run_plugins_on_preprocess = functools.partial(_run_filetype_plugins,
occasion='preprocess')
run_plugins_on_postprocess = functools.partial(_run_filetype_plugins,
occasion='postprocess')
def initialize_plugin(plugin, path_to_zip_file):
print 'Initializing plugin', plugin.name
try:
plugin(path_to_zip_file)
except Exception:
tb = traceback.format_exc()
raise InvalidPlugin((_('Initialization of plugin %s failed with traceback:')
%tb) + '\n'+tb)
def add_plugin(path_to_zip_file):
make_config_dir()
plugin = load_plugin(path_to_zip_file)
initialize_plugin(plugin, path_to_zip_file)
plugins = config['plugins']
zfp = os.path.join(plugin_dir, 'name.zip')
if os.path.exists(zfp):
os.remove(zfp)
shutil.copyfile(path_to_zip_file, zfp)
plugins[plugin.name] = zfp
config['plugins'] = plugins
initialize_plugins()
return plugin
def is_disabled(plugin):
return plugin.name in config['disabled_plugins']
def find_plugin(name):
for plugin in _initialized_plugins:
if plugin.name == name:
return plugin
def disable_plugin(plugin_or_name):
x = getattr(plugin_or_name, 'name', plugin_or_name)
dp = config['disabled_plugins']
dp.add(x)
config['disabled_plugins'] = dp
def enable_plugin(plugin_or_name):
x = getattr(plugin_or_name, 'name', plugin_or_name)
dp = config['disabled_plugins']
if x in dp:
dp.remove(x)
config['disabled_plugins'] = dp
def initialize_plugins():
global _initialized_plugins
_initialized_plugins = []
for zfp in config['plugins'].values():
try:
plugin = load_plugin(zfp)
initialize_plugin(plugin, zfp)
_initialized_plugins.append(plugin)
except:
print 'Failed to initialize plugin...'
traceback.print_exc()
_initialized_plugins.sort(cmp=lambda x,y:cmp(x.priority, y.priority), reverse=True)
reread_filetype_plugins()
initialize_plugins()
def option_parser():
parser = OptionParser(usage=_('''\
%prog options
Customize calibre by loading external plugins.
'''))
parser.add_option('-a', '--add-plugin', default=None,
help=_('Add a plugin by specifying the path to the zip file containing it.'))
parser.add_option('--customize-plugin', default=None,
help=_('Customize plugin. Specify name of plugin and customization string separated by a comma.'))
parser.add_option('-l', '--list-plugins', default=False, action='store_true',
help=_('List all installed plugins'))
parser.add_option('--enable-plugin', default=None,
help=_('Enable the named plugin'))
parser.add_option('--disable-plugin', default=None,
help=_('Disable the named plugin'))
return parser
def main(args=sys.argv):
parser = option_parser()
if len(args) < 2:
parser.print_help()
return 1
opts, args = parser.parse_args(args)
if opts.add_plugin is not None:
plugin = add_plugin(opts.add_plugin)
print 'Plugin added:', plugin.name, plugin.version
if opts.customize_plugin is not None:
name, custom = opts.customize_plugin.split(',')
plugin = find_plugin(name.strip())
if plugin is None:
print 'No plugin with the name %s exists'%name
return 1
config['plugin_customization'][plugin.name] = custom.strip()
if opts.enable_plugin is not None:
enable_plugin(opts.enable_plugin.strip())
if opts.disable_plugin is not None:
disable_plugin(opts.disable_plugin.strip())
if opts.list_plugins:
print 'Name\tVersion\tDisabled\tLocal Customization'
for plugin in _initialized_plugins:
print '%s\t%s\t%s\t%s'%(plugin.name, plugin.version, is_disabled(plugin),
config['plugin_customization'].get(plugin.name))
print '\t', plugin.customization_help()
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -21,6 +21,8 @@ Run an embedded python interpreter.
'Module specifications are of the form full.name.of.module,path_to_module.py', default=None 'Module specifications are of the form full.name.of.module,path_to_module.py', default=None
) )
parser.add_option('-c', '--command', help='Run python code.', default=None) parser.add_option('-c', '--command', help='Run python code.', default=None)
parser.add_option('-g', '--run-gui', help='Run the GUI', default=False,
action='store_true')
parser.add_option('--migrate', action='store_true', default=False, parser.add_option('--migrate', action='store_true', default=False,
help='Migrate old database. Needs two arguments. Path to library1.db and path to new library folder.') help='Migrate old database. Needs two arguments. Path to library1.db and path to new library folder.')
return parser return parser
@ -72,7 +74,10 @@ def migrate(old, new):
def main(args=sys.argv): def main(args=sys.argv):
opts, args = option_parser().parse_args(args) opts, args = option_parser().parse_args(args)
if opts.update_module: if opts.run_gui:
from calibre.gui2.main import main
main()
elif opts.update_module:
mod, path = opts.update_module.partition(',')[0], opts.update_module.partition(',')[-1] mod, path = opts.update_module.partition(',')[0], opts.update_module.partition(',')[-1]
update_module(mod, os.path.expanduser(path)) update_module(mod, os.path.expanduser(path))
elif opts.command: elif opts.command:

View File

@ -63,6 +63,7 @@ entry_points = {
'calibredb = calibre.library.cli:main', 'calibredb = calibre.library.cli:main',
'calibre-fontconfig = calibre.utils.fontconfig:main', 'calibre-fontconfig = calibre.utils.fontconfig:main',
'calibre-parallel = calibre.parallel:main', 'calibre-parallel = calibre.parallel:main',
'calibre-customize = calibre.customize.ui:main',
], ],
'gui_scripts' : [ 'gui_scripts' : [
__appname__+' = calibre.gui2.main:main', __appname__+' = calibre.gui2.main:main',

View File

@ -28,9 +28,11 @@ else:
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config'))) bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
config_dir = os.path.join(bdir, 'calibre') config_dir = os.path.join(bdir, 'calibre')
plugin_dir = os.path.join(config_dir, 'plugins')
def make_config_dir(): def make_config_dir():
if not os.path.exists(config_dir): if not os.path.exists(plugin_dir):
os.makedirs(config_dir, mode=448) # 0700 == 448 os.makedirs(plugin_dir, mode=448) # 0700 == 448
class CustomHelpFormatter(IndentedHelpFormatter): class CustomHelpFormatter(IndentedHelpFormatter):
@ -78,6 +80,7 @@ class OptionParser(_OptionParser):
gui_mode=False, gui_mode=False,
conflict_handler='resolve', conflict_handler='resolve',
**kwds): **kwds):
usage = textwrap.dedent(usage)
usage += '''\n\nWhenever you pass arguments to %prog that have spaces in them, '''\ usage += '''\n\nWhenever you pass arguments to %prog that have spaces in them, '''\
'''enclose the arguments in quotation marks.''' '''enclose the arguments in quotation marks.'''
_OptionParser.__init__(self, usage=usage, version=version, epilog=epilog, _OptionParser.__init__(self, usage=usage, version=version, epilog=epilog,