From ae1c331d3c3747782df535853a103a18d6886da8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 07:53:17 -0600 Subject: [PATCH] Start work on an example plugin demonstrating the new plugin loader --- setup/upload.py | 19 ++- src/calibre/customize/zipplugin.py | 84 +++++++++- src/calibre/gui2/ui.py | 3 + src/calibre/manual/creating_plugins.rst | 60 +++++++ src/calibre/manual/customize.rst | 151 ++---------------- .../plugin_examples/helloworld/__init__.py | 33 ++++ .../plugin_examples/interface/__init__.py | 33 ++++ .../plugin_examples/interface/images/icon.png | Bin 0 -> 5785 bytes .../plugin-import-name-interface.txt | 0 .../manual/plugin_examples/interface/ui.py | 30 ++++ src/calibre/manual/tutorials.rst | 1 + 11 files changed, 265 insertions(+), 149 deletions(-) create mode 100644 src/calibre/manual/creating_plugins.rst create mode 100644 src/calibre/manual/plugin_examples/helloworld/__init__.py create mode 100644 src/calibre/manual/plugin_examples/interface/__init__.py create mode 100644 src/calibre/manual/plugin_examples/interface/images/icon.png create mode 100644 src/calibre/manual/plugin_examples/interface/plugin-import-name-interface.txt create mode 100644 src/calibre/manual/plugin_examples/interface/ui.py diff --git a/setup/upload.py b/setup/upload.py index 3bad1dd8f3..468c0a61b3 100644 --- a/setup/upload.py +++ b/setup/upload.py @@ -1,14 +1,14 @@ #!/usr/bin/env python # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai -from __future__ import with_statement __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os, re, cStringIO, base64, httplib, subprocess, hashlib, shutil, time +import os, re, cStringIO, base64, httplib, subprocess, hashlib, shutil, time, glob from subprocess import check_call from tempfile import NamedTemporaryFile, mkdtemp +from zipfile import ZipFile from setup import Command, __version__, installer_name, __appname__ @@ -341,7 +341,22 @@ class UploadUserManual(Command): # {{{ description = 'Build and upload the User Manual' sub_commands = ['manual'] + def build_plugin_example(self, path): + from calibre import CurrentDir + with NamedTemporaryFile(suffix='.zip') as f: + with CurrentDir(self.d(path)): + with ZipFile(f, 'w') as zf: + for x in os.listdir('.'): + zf.write(x) + bname = self.b(path) + '_plugin.zip' + subprocess.check_call(['scp', f.name, 'divok:%s/%s'%(DOWNLOADS, + bname)]) + def run(self, opts): + path = self.j(self.SRC, 'calibre', 'manual', 'plugin_examples') + for x in glob.glob(self.j(path, '*')): + self.build_plugin_example(x) + check_call(' '.join(['scp', '-r', 'src/calibre/manual/.build/html/*', 'divok:%s'%USER_MANUAL]), shell=True) # }}} diff --git a/src/calibre/customize/zipplugin.py b/src/calibre/customize/zipplugin.py index 0df64ce79c..932337f624 100644 --- a/src/calibre/customize/zipplugin.py +++ b/src/calibre/customize/zipplugin.py @@ -10,6 +10,7 @@ __docformat__ = 'restructuredtext en' import os, zipfile, posixpath, importlib, threading, re, imp, sys from collections import OrderedDict +from functools import partial from calibre.customize import (Plugin, numeric_version, platform, InvalidPlugin, PluginNotFound) @@ -18,6 +19,65 @@ from calibre.customize import (Plugin, numeric_version, platform, # python 2.x that prevents importing from zip files in locations whose paths # have non ASCII characters +def get_resources(zfp, name_or_list_of_names): + ''' + Load resources from the plugin zip file + + :param name_or_list_of_names: List of paths to resources in the zip file using / as + separator, or a single path + + :return: A dictionary of the form ``{name : file_contents}``. Any names + that were not found in the zip file will not be present in the + dictionary. If a single path is passed in the return value will + be just the bytes of the resource or None if it wasn't found. + ''' + names = name_or_list_of_names + if isinstance(names, basestring): + names = [names] + ans = {} + with zipfile.ZipFile(zfp) as zf: + for name in names: + try: + ans[name] = zf.read(name) + except: + pass + if len(names) == 1: + ans = ans.pop(names[0], None) + + return ans + +def get_icons(zfp, name_or_list_of_names): + ''' + Load icons from the plugin zip file + + :param name_or_list_of_names: List of paths to resources in the zip file using / as + separator, or a single path + + :return: A dictionary of the form ``{name : QIcon}``. Any names + that were not found in the zip file will be null QIcons. + If a single path is passed in the return value will + be A QIcon. + ''' + from PyQt4.Qt import QIcon, QPixmap + names = name_or_list_of_names + ans = get_resources(zfp, names) + if isinstance(names, basestring): + names = [names] + if ans is None: + ans = {} + if isinstance(ans, basestring): + ans = dict([(names[0], ans)]) + + ians = {} + for name in names: + p = QPixmap() + raw = ans.get('name', None) + if raw: + p.loadFromData(raw) + ians[name] = QIcon(p) + if len(names) == 1: + ians = ians.pop(names[0]) + return ians class PluginLoader(object): @@ -33,9 +93,10 @@ class PluginLoader(object): return parts[0], None plugin_name = parts[1] with self._lock: - names = self.loaded_plugins.get(plugin_name, None)[1] + names = self.loaded_plugins.get(plugin_name, None) if names is None: raise ImportError('No plugin named %r loaded'%plugin_name) + names = names[1] fullname = '.'.join(parts[2:]) if not fullname: fullname = '__init__' @@ -77,6 +138,8 @@ class PluginLoader(object): with zipfile.ZipFile(zfp) as zf: code = zf.read(zinfo) compiled = compile(code, 'import_name', 'exec', dont_inherit=True) + mod.__dict__['get_resources'] = partial(get_resources, zfp) + mod.__dict__['get_icons'] = partial(get_icons, zfp) exec compiled in mod.__dict__ return mod @@ -139,10 +202,6 @@ class PluginLoader(object): if plugin_name not in self.loaded_plugins: break else: - if plugin_name in self.loaded_plugins: - raise InvalidPlugin(( - 'The plugin in %r uses an import name %r that is already' - ' used by another plugin') % (path_to_zip_file, plugin_name)) if self._identifier_pat.match(plugin_name) is None: raise InvalidPlugin(( 'The plugin at %r uses an invalid import name: %r' % @@ -194,3 +253,18 @@ loader = PluginLoader() sys.meta_path.insert(0, loader) +if __name__ == '__main__': + from tempfile import NamedTemporaryFile + from calibre.customize.ui import add_plugin + from calibre import CurrentDir + path = sys.argv[-1] + with NamedTemporaryFile(suffix='.zip') as f: + with zipfile.ZipFile(f, 'w') as zf: + with CurrentDir(path): + for x in os.listdir('.'): + if x[0] != '.': + print ('Adding', x) + zf.write(x) + add_plugin(f.name) + print ('Added plugin from', sys.argv[-1]) + diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 03aae37883..a0aa0138bc 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -382,6 +382,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ error_dialog(self, _('Failed to start content server'), unicode(self.content_server.exception)).exec_() + @property + def current_db(self): + return self.library_view.model().db def another_instance_wants_to_talk(self): try: diff --git a/src/calibre/manual/creating_plugins.rst b/src/calibre/manual/creating_plugins.rst new file mode 100644 index 0000000000..ea1d519864 --- /dev/null +++ b/src/calibre/manual/creating_plugins.rst @@ -0,0 +1,60 @@ + +.. include:: global.rst + +.. _pluginstutorial: + +Writing your own plugins to extend |app|'s functionality +==================================================================== + +|app| has a very modular design. Almost all functionality in |app| comes in the form of plugins. Plugins are used for conversion, for downloading news (though these are called recipes), for various components of the user interface, to connect to different devices, to process files when adding them to |app| and so on. You can get a complete list of all the builtin plugins in |app| by going to :guilabel:`Preferences->Plugins`. + +Here, we will teach you how to create your own plugins to add new features to |app|. + + +.. contents:: Contents + :depth: 2 + :local: + +.. note:: This only applies to calibre releases >= 0.7.52 + +Anatomy of a |app| plugin +--------------------------- + +A |app| plugin is very simple, it's just a zip file that contains some python code +and any other resources like image files needed by the plugin. Without further ado, +let's see a basic example. + +Suppose you have an installation of |app| that you are using to self publish various e-documents in EPUB and MOBI +formats. You would like all files generated by |app| to have their publisher set as "Hello world", here's how to do it. +Create a file named :file:`__init__.py` (this is a special name and must always be used for the main file of your plugin) +and enter the following Python code into it: + +.. literalinclude:: plugin_examples/helloworld/__init__.py + :lines: 10- + +That's all. To add this code to |app| as a plugin, simply create a zip file with:: + + zip plugin.zip __init__.py + +Add this plugin to |app| via :guilabel:`Preferences->Plugins`. + +You can download the Hello World plugin from +`helloworld_plugin.zip `_. + +Every time you use calibre to convert a book, the plugin's :meth:`run` method will be called and the +converted book will have its publisher set to "Hello World". This is a trivial plugin, lets move on to +a more complex example that actually adds a component to the user interface. + +A User Interface plugin +------------------------- + +This plugin will be spread over a couple of files (to keep the code clean). It will show you how to get resources +(images or data files) from the plugin zip file, how to create elements in the |app| user interface and how to access +and query the books database in |app|. + +The different types of plugins +-------------------------------- + +As you may have noticed above, a plugin in |app| is a class. There are different classes for the different types of plugins in |app|. +Details on each class, including the base class of all plugins can be found in :ref:`plugins`. + diff --git a/src/calibre/manual/customize.rst b/src/calibre/manual/customize.rst index 6218bf8112..fe33100576 100644 --- a/src/calibre/manual/customize.rst +++ b/src/calibre/manual/customize.rst @@ -17,6 +17,11 @@ use *plugins* to add functionality to |app|. :depth: 2 :local: +.. toctree:: + :hidden: + + plugins + Environment variables ----------------------- @@ -53,148 +58,10 @@ You should not change the files in this resources folder, as your changes will g For example, if you wanted to change the icon for the :guilabel:`Remove books` action, you would first look in the builtin resources folder and see that the relevant file is :file:`resources/images/trash.svg`. Assuming you have an alternate icon in svg format called :file:`mytrash.svg` you would save it in the configuration directory as :file:`resources/images/trash.svg`. All the icons used by the calibre user interface are in :file:`resources/images` and its sub-folders. -A Hello World plugin ------------------------- +Customizing |app| with plugins +-------------------------------- -Suppose you have an installation of |app| that you are using to self publish various e-documents in EPUB and LRF -format. You would like all file generated by |app| to have their publisher set as "Hello world", here's how to do it. -Create a file name :file:`my_plugin.py` (the file name must end with plugin.py) and enter the following Python code into it: +|app| has a very modular design. Almost all functionality in |app| comes in the form of plugins. Plugins are used for conversion, for downloading news (though these are called recipes), for various components of the user interface, to connect to different devices, to process files when adding them to |app| and so on. You can get a complete list of all the builtin plugins in |app| by going to :guilabel:`Preferences->Plugins`. -.. code-block:: python +You can write your own plugins to customize and extend the behavior of |app|. The plugin architecture in |app| is very simple, see the tutorial :ref:`pluginstutorial`. - import os - from calibre.customize import FileTypePlugin - - class HelloWorld(FileTypePlugin): - - name = 'Hello World Plugin' # Name of the plugin - description = 'Set the publisher to Hello World for all new conversions' - supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on - author = 'Acme Inc.' # The author of this plugin - version = (1, 0, 0) # The version number of this plugin - file_types = set(['epub', 'lrf']) # The file types that this plugin will be applied to - on_postprocess = True # Run this plugin after conversion is complete - - def run(self, path_to_ebook): - from calibre.ebooks.metadata.meta import get_metadata, set_metadata - file = open(path_to_ebook, 'r+b') - ext = os.path.splitext(path_to_ebook)[-1][1:].lower() - mi = get_metadata(file, ext) - mi.publisher = 'Hello World' - set_metadata(file, mi, ext) - return path_to_ebook - -That's all. To add this code to |app| as a plugin, simply create a zip file with:: - - zip plugin.zip my_plugin.py - -You can download the Hello World plugin from -`helloworld_plugin.zip `_. -Now either use the configuration dialog in |app| GUI to add this zip file as a plugin, or -use the command:: - - calibre-customize -a plugin.zip - -Every time you use calibre to convert a book, the plugin's :meth:`run` method will be called and the -converted book will have its publisher set to "Hello World". For more information about -|app|'s plugin system, read on... - - -A Hello World GUI plugin ---------------------------- - -Here's a simple Hello World plugin for the |app| GUI. It will cause a box to popup with the message "Hellooo World!" when you press Ctrl+Shift+H - -.. note:: Only available in calibre versions ``>= 0.7.32``. - -.. code-block:: python - - from calibre.customize import InterfaceActionBase - - class HelloWorldBase(InterfaceActionBase): - - name = 'Hello World GUI' - author = 'The little green man' - - def load_actual_plugin(self, gui): - from calibre.gui2.actions import InterfaceAction - - class HelloWorld(InterfaceAction): - name = 'Hello World GUI' - action_spec = ('Hello World!', 'add_book.png', None, - _('Ctrl+Shift+H')) - - def genesis(self): - self.qaction.triggered.connect(self.hello_world) - - def hello_world(self, *args): - from calibre.gui2 import info_dialog - info_dialog(self.gui, 'Hello World!', 'Hellooo World!', - show=True) - - return HelloWorld(gui, self.site_customization) - -You can also have it show up in the toolbars/context menu by going to Preferences->Toolbars and adding this plugin to the locations you want it to be in. - -While this plugin is utterly useless, note that all calibre GUI actions like adding/saving/removing/viewing/etc. are implemented as plugins, so there is no limit to what you can achieve. The key thing to remember is that the plugin has access to the full |app| GUI via ``self.gui``. - - -The Plugin base class ------------------------- - -As you may have noticed above, all |app| plugins are classes. The Plugin classes are organized in a hierarchy at the top of which -is :class:`calibre.customize.Plugin`. The has excellent in source documentation for its various features, here I will discuss a -few of the important ones. - -First, all plugins must supply a list of platforms they have been tested on by setting the ``supported_platforms`` member as in the -example above. - -If the plugin needs to do any initialization, it should implement the :meth:`initialize` method. The path to the plugin zip file -is available as ``self.plugin_path``. The initialization method could be used to load any needed resources from the zip file. - -If the plugin needs to be customized (i.e. it needs some information from the user), it should implement the :meth:`customization_help` -method, to indicate to |app| that it needs user input. This can be useful, for example, to ask the user to input the path to a needed system -binary or the URL of a website, etc. When |app| asks the user for the customization information, the string retuned by the :meth:`customization_help` -method is used as help text to le thte user know what information is needed. - -Another useful method is :meth:`temporary_file`, which returns a file handle to an opened temporary file. If your plugin needs to make use -of temporary files, it should use this method. Temporary file cleanup is then taken care of automatically. - -In addition, whenever plugins are run, their zip files are automatically added to the start of ``sys.path``, so you can directly import -any python files you bundle in the zip files. Note that this is not available when the plugin is being initialized, only when it is being run. - -Finally, plugins can have a priority (a positive integer). Higher priority plugins are run in preference tolower priority ones in a given context. -By default all plugins have priority 1. You can change that by setting the member :attr:'priority` in your subclass. - -See :ref:`pluginsPlugin` for details. - -File type plugins -------------------- - -File type plugins are intended to be associated with specific file types (as identified by extension). They can be run on several different occassions. - - * When books/formats are added ot the |app| database (if :attr:`on_import` is set to True). - * Just before an any2whatever converter is run on an input file (if :attr:`on_preprocess` is set to True). - * After an any2whatever converter has run, on the output file (if :attr:`on_postprocess` is set to True). - -File type plugins specify which file types they are associated with by specifying the :attr:`file_types` member as in the above example. -the actual work should be done in the :meth:`run` method, which must return the path to the modified ebook (it can be the same as the original -if the modifcations are done in place). - -See :ref:`pluginsFTPlugin` for details. - -Metadata plugins -------------------- - -Metadata plugins add the ability to read/write metadata from ebook files to |app|. See :ref:`pluginsMetadataPlugin` for details. - -.. toctree:: - :hidden: - - plugins - -Metadata download plugins ----------------------------- - -Metadata download plugins add various sources that |app| uses to download metadata based on title/author/isbn etc. See :ref:`pluginsMetadataSource` -for details. diff --git a/src/calibre/manual/plugin_examples/helloworld/__init__.py b/src/calibre/manual/plugin_examples/helloworld/__init__.py new file mode 100644 index 0000000000..2cb3236b7b --- /dev/null +++ b/src/calibre/manual/plugin_examples/helloworld/__init__.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os +from calibre.customize import FileTypePlugin + +class HelloWorld(FileTypePlugin): + + name = 'Hello World Plugin' # Name of the plugin + description = 'Set the publisher to Hello World for all new conversions' + supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on + author = 'Acme Inc.' # The author of this plugin + version = (1, 0, 0) # The version number of this plugin + file_types = set(['epub', 'mobi']) # The file types that this plugin will be applied to + on_postprocess = True # Run this plugin after conversion is complete + minimum_calibre_version = (0, 7, 51) + + def run(self, path_to_ebook): + from calibre.ebooks.metadata.meta import get_metadata, set_metadata + file = open(path_to_ebook, 'r+b') + ext = os.path.splitext(path_to_ebook)[-1][1:].lower() + mi = get_metadata(file, ext) + mi.publisher = 'Hello World' + set_metadata(file, mi, ext) + return path_to_ebook + + diff --git a/src/calibre/manual/plugin_examples/interface/__init__.py b/src/calibre/manual/plugin_examples/interface/__init__.py new file mode 100644 index 0000000000..56b2c1247e --- /dev/null +++ b/src/calibre/manual/plugin_examples/interface/__init__.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +# The class that all Interface Action plugin wrappers must inherit from +from calibre.customize import InterfaceActionBase + +class InterfacePluginDemo(InterfaceActionBase): + ''' + This class is a simple wrapper that provides information about the actual + plugin class. The actual interface plugin class is called InterfacePlugin + and is defined in the ui.py file, as specified in the actual_plugin field + below. + + The reason for having two classes is that it allows the command line + calibre utilities to run without needing to load the GUI libraries. + ''' + name = 'Interface Plugin Demo' + description = 'An advanced plugin demo' + supported_platforms = ['windows', 'osx', 'linux'] + author = 'Kovid Goyal' + version = (1, 0, 0) + minimum_calibre_version = (0, 7, 51) + + #: This field defines the plugin class that contains all the code + #: that actually does something. + actual_plugin = 'calibre_plugins.interface.ui:InterfacePlugin' + diff --git a/src/calibre/manual/plugin_examples/interface/images/icon.png b/src/calibre/manual/plugin_examples/interface/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ad823e2ff4d53c376632ccb0e2fe60d96cc063ea GIT binary patch literal 5785 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