From 03cc260fe5a2ff6ae3aa9e16271da10f52555f7d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 11:00:54 -0600 Subject: [PATCH] Complete the example plugin and plugin creating tutorial --- setup/upload.py | 3 + src/calibre/customize/zipplugin.py | 17 ++- src/calibre/manual/creating_plugins.rst | 100 ++++++++++++++++- .../manual/plugin_examples/interface/ui.py | 30 ------ .../{interface => interface_demo}/__init__.py | 7 +- .../plugin_examples/interface_demo/about.txt | 7 ++ .../images/icon.png | Bin .../plugin_examples/interface_demo/main.py | 102 ++++++++++++++++++ .../plugin-import-name-interface_demo.txt} | 0 .../plugin_examples/interface_demo/ui.py | 58 ++++++++++ 10 files changed, 286 insertions(+), 38 deletions(-) delete mode 100644 src/calibre/manual/plugin_examples/interface/ui.py rename src/calibre/manual/plugin_examples/{interface => interface_demo}/__init__.py (79%) create mode 100644 src/calibre/manual/plugin_examples/interface_demo/about.txt rename src/calibre/manual/plugin_examples/{interface => interface_demo}/images/icon.png (100%) create mode 100644 src/calibre/manual/plugin_examples/interface_demo/main.py rename src/calibre/manual/plugin_examples/{interface/plugin-import-name-interface.txt => interface_demo/plugin-import-name-interface_demo.txt} (100%) create mode 100644 src/calibre/manual/plugin_examples/interface_demo/ui.py diff --git a/setup/upload.py b/setup/upload.py index 468c0a61b3..6cd9ad3eca 100644 --- a/setup/upload.py +++ b/setup/upload.py @@ -348,6 +348,9 @@ class UploadUserManual(Command): # {{{ with ZipFile(f, 'w') as zf: for x in os.listdir('.'): zf.write(x) + if os.path.isdir(x): + for y in os.listdir(x): + zf.write(os.path.join(x, y)) bname = self.b(path) + '_plugin.zip' subprocess.check_call(['scp', f.name, 'divok:%s/%s'%(DOWNLOADS, bname)]) diff --git a/src/calibre/customize/zipplugin.py b/src/calibre/customize/zipplugin.py index 932337f624..7f23cf46e2 100644 --- a/src/calibre/customize/zipplugin.py +++ b/src/calibre/customize/zipplugin.py @@ -40,7 +40,8 @@ def get_resources(zfp, name_or_list_of_names): try: ans[name] = zf.read(name) except: - pass + import traceback + traceback.print_exc() if len(names) == 1: ans = ans.pop(names[0], None) @@ -71,7 +72,7 @@ def get_icons(zfp, name_or_list_of_names): ians = {} for name in names: p = QPixmap() - raw = ans.get('name', None) + raw = ans.get(name, None) if raw: p.loadFromData(raw) ians[name] = QIcon(p) @@ -136,8 +137,13 @@ class PluginLoader(object): raise ImportError('Plugin %r has no module named %r' % (plugin_name, import_name)) with zipfile.ZipFile(zfp) as zf: - code = zf.read(zinfo) - compiled = compile(code, 'import_name', 'exec', dont_inherit=True) + try: + code = zf.read(zinfo) + except: + # Maybe the zip file changed from under us + code = zf.read(zinfo.filename) + compiled = compile(code, 'calibre_plugins.%s.%s'%(plugin_name, + 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__ @@ -265,6 +271,9 @@ if __name__ == '__main__': if x[0] != '.': print ('Adding', x) zf.write(x) + if os.path.isdir(x): + for y in os.listdir(x): + zf.write(os.path.join(x, y)) add_plugin(f.name) print ('Added plugin from', sys.argv[-1]) diff --git a/src/calibre/manual/creating_plugins.rst b/src/calibre/manual/creating_plugins.rst index ea1d519864..d365f10ed0 100644 --- a/src/calibre/manual/creating_plugins.rst +++ b/src/calibre/manual/creating_plugins.rst @@ -48,13 +48,111 @@ 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 +This plugin will be spread over a few 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|. +You can download this plugin from `interface_demo_plugin.zip `_ + +The first thing to note is that this zip file has a lot more files in it, explained below, pay particular attention to +``plugin-import-name-interface_demo.txt``. + + **plugin-import-name-interface_demo.txt** + An empty text file used to enable the multi-file plugin magic. This file must be present in all plugins that use + more than one .py file. It should be empty and its filename must be of the form: plugin-import-name-**some_name**.txt + The presence of this file allows you to import code from the .py files present inside the zip file, using a statement like:: + + from calibre_plugins.some_name.some_module import some_object + + The prefix ``calibre_plugins`` must always be present. ``some_name`` comes from the filename of the empty text file. + ``some_module`` refers to :file:`some_module.py` file inside the zip file. Note that this importing is just as + powerful as regular python imports. You can create packages and subpackages of .py modules inside the zip file, + just like you would normally (by defining __init__.py in each sub directory), and everything should Just Work. + + The name you use for ``some_name`` enters a global namespace shared by all plugins, **so make it as unique as possible**. + But remember that it must be a valid python identifier (only alphabets, numbers and the underscore). + + **__init__.py** + As before, the file that defines the plugin class + + **main.py** + This file contains the actual code that does something useful + + **ui.py** + This file defines the interface part of the plugin + + **images/icon.png** + The icon for this plugin + + **about.txt** + A text file with information about the plugin + +Now let's look at the code. + +__init__.py +^^^^^^^^^^^^^ + +First, the obligatory ``__init__.py`` to define the plugin metadata: + +.. literalinclude:: plugin_examples/interface_demo/__init__.py + :lines: 10- + +The only noteworthy feature is the field :attr:`actual_plugin`. Since |app| has both command line and GUI interfaces, +GUI plugins like this one should not load any GUI libraries in __init__.py. The actual_plugin field does this for you, +by telling |app| that the actual plugin is to be found in another file inside your zip archive, which will only be loaded +in a GUI context. + +Remember that for this to work, you must have a plugin-import-name-some_name.txt file in your plugin zip file, +as discussed above. + + +ui.py +^^^^^^^^ + +Now let's look at ui.py which defines the actual GUI plugin. The source code is heavily commented and should be self explanatory: + +.. literalinclude:: plugin_examples/interface_demo/ui.py + :lines: 10- + +main.py +^^^^^^^^^ + +The actual logic to implement the Interface Plugin Demo dialog. + +.. literalinclude:: plugin_examples/interface_demo/main.py + :lines: 10- + +Getting resources from the plugin zip file +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|app|'s plugin loading system defines a couple of builtin functions that allow you to conveniently get files from the plugin zip file. + + **get_resources(name_or_list_of_names)** + This function should be called with a list of paths to files inside the zip file. For example to access the file icon.png in + the directory images in the zip file, you would use: ``images/icon.png``. Always use a forward slash as the path separator, + even on windows. When you pass in a single name, the function will return the raw bytes of that file or None if the name + was not found in the zip file. If you pass in more than one name then it returns a dict mapping the names to bytes. + If a name is not found, it will not be present in the returned dict. + + **get_icons(name_or_list_of_names)** + A convenience wrapper for get_resources() that creates QIcon objects from the raw bytes returned by get_resources. + If a name is not found in the zip file the corresponding QIcon will be null. + + 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`. +More plugin examples +---------------------- + +You can find a list of many, sophisticated |app| plugins `here `_. + +Sharing your plugins with others +---------------------------------- + +If you would like to share the plugins you have created with other users of |app|, post your plugin in a new thread in the +`calibre plugins forum `_. + diff --git a/src/calibre/manual/plugin_examples/interface/ui.py b/src/calibre/manual/plugin_examples/interface/ui.py deleted file mode 100644 index dc6e03ce65..0000000000 --- a/src/calibre/manual/plugin_examples/interface/ui.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/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 plugins must inherit from -from calibre.gui2.actions import InterfaceAction - -if False: - # This is here to keep my python error checker from complaining about - # the builtins that will be defined by the plugin loading system - get_icons = None - -class InterfacePlugin(InterfaceAction): - - name = 'Interface Plugin Demo' - - action_spec = ('Interface Plugin Demo', None, - 'Run the Interface Plugin Demo', 'Ctrl+Shift+F1') - - def genesis(self): - # This method is called once per plugin, do initial setup here - print (1111, get_icons('icon.png')) - self.qaction.setIcon(get_icons('icon.png')) - diff --git a/src/calibre/manual/plugin_examples/interface/__init__.py b/src/calibre/manual/plugin_examples/interface_demo/__init__.py similarity index 79% rename from src/calibre/manual/plugin_examples/interface/__init__.py rename to src/calibre/manual/plugin_examples/interface_demo/__init__.py index 56b2c1247e..dc33f4e427 100644 --- a/src/calibre/manual/plugin_examples/interface/__init__.py +++ b/src/calibre/manual/plugin_examples/interface_demo/__init__.py @@ -27,7 +27,8 @@ class InterfacePluginDemo(InterfaceActionBase): 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' + #: This field defines the GUI plugin class that contains all the code + #: that actually does something. Its format is module_path:class_name + #: The specified class must be defined in the specified module. + actual_plugin = 'calibre_plugins.interface_demo.ui:InterfacePlugin' diff --git a/src/calibre/manual/plugin_examples/interface_demo/about.txt b/src/calibre/manual/plugin_examples/interface_demo/about.txt new file mode 100644 index 0000000000..8ab957e322 --- /dev/null +++ b/src/calibre/manual/plugin_examples/interface_demo/about.txt @@ -0,0 +1,7 @@ +The Interface Plugin Demo +=========================== + +Created by Kovid Goyal + +Requires calibre >= 0.7.52 + diff --git a/src/calibre/manual/plugin_examples/interface/images/icon.png b/src/calibre/manual/plugin_examples/interface_demo/images/icon.png similarity index 100% rename from src/calibre/manual/plugin_examples/interface/images/icon.png rename to src/calibre/manual/plugin_examples/interface_demo/images/icon.png diff --git a/src/calibre/manual/plugin_examples/interface_demo/main.py b/src/calibre/manual/plugin_examples/interface_demo/main.py new file mode 100644 index 0000000000..de668799aa --- /dev/null +++ b/src/calibre/manual/plugin_examples/interface_demo/main.py @@ -0,0 +1,102 @@ +#!/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' + + +from PyQt4.Qt import QDialog, QVBoxLayout, QPushButton, QMessageBox + +if False: + # This is here to keep my python error checker from complaining about + # the builtin functions that will be defined by the plugin loading system + get_icons = get_resources = None + +class DemoDialog(QDialog): + + def __init__(self, gui, icon): + QDialog.__init__(self, gui) + self.gui = gui + + # The current database shown in the GUI + # db is an instance of the class LibraryDatabase2 from database.py + # This class has many, many methods that allow you to do a lot of + # things. + self.db = gui.current_db + + self.l = QVBoxLayout() + self.setLayout(self.l) + + + self.setWindowTitle('Interface Plugin Demo') + self.setWindowIcon(icon) + + self.about_button = QPushButton('About', self) + self.about_button.clicked.connect(self.about) + self.l.addWidget(self.about_button) + + self.marked_button = QPushButton( + 'Show books with only one format in the calibre GUI', self) + self.marked_button.clicked.connect(self.marked) + self.l.addWidget(self.marked_button) + + self.view_button = QPushButton( + 'View the most recently added book', self) + self.view_button.clicked.connect(self.view) + self.l.addWidget(self.view_button) + + self.resize(self.sizeHint()) + + def about(self): + # Get the about text from a file inside the plugin zip file + # The get_resources function is a builtin function defined for all your + # plugin code. It loads files from the plugin zip file. It returns + # the bytes from the specified file. + # + # Note that if you are loading more than one file, for performance, you + # should pass a list of names to get_resources. In this case, + # get_resources will return a dictionary mapping names to bytes. Names that + # are not found in the zip file will not be in the returned dictionary. + text = get_resources('about.txt') + QMessageBox.about(self, 'About the Interface Plugin Demo', + text.decode('utf-8')) + + def marked(self): + fmt_idx = self.db.FIELD_MAP['formats'] + matched_ids = set() + for record in self.db.data.iterall(): + # Iterate over all records + fmts = record[fmt_idx] + # fmts is either None or a comma separated list of formats + if fmts and ',' not in fmts: + matched_ids.add(record[0]) + # Mark the records with the matching ids + self.db.set_marked_ids(matched_ids) + + # Tell the GUI to search for all marked records + self.gui.search.setEditText('marked:true') + self.gui.search.do_search() + + def view(self): + most_recent = most_recent_id = None + timestamp_idx = self.db.FIELD_MAP['timestamp'] + + for record in self.db.data: + # Iterate over all currently showing records + timestamp = record[timestamp_idx] + if most_recent is None or timestamp > most_recent: + most_recent = timestamp + most_recent_id = record[0] + + if most_recent_id is not None: + # Get the row number of the id as shown in the GUI + row_number = self.db.row(most_recent_id) + # Get a reference to the View plugin + view_plugin = self.gui.iactions['View'] + # Ask the view plugin to launch the viewer for row_number + view_plugin._view_books([row_number]) + + diff --git a/src/calibre/manual/plugin_examples/interface/plugin-import-name-interface.txt b/src/calibre/manual/plugin_examples/interface_demo/plugin-import-name-interface_demo.txt similarity index 100% rename from src/calibre/manual/plugin_examples/interface/plugin-import-name-interface.txt rename to src/calibre/manual/plugin_examples/interface_demo/plugin-import-name-interface_demo.txt diff --git a/src/calibre/manual/plugin_examples/interface_demo/ui.py b/src/calibre/manual/plugin_examples/interface_demo/ui.py new file mode 100644 index 0000000000..b3535da925 --- /dev/null +++ b/src/calibre/manual/plugin_examples/interface_demo/ui.py @@ -0,0 +1,58 @@ +#!/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 plugins must inherit from +from calibre.gui2.actions import InterfaceAction +from calibre_plugins.interface_demo.main import DemoDialog + +if False: + # This is here to keep my python error checker from complaining about + # the builtin functions that will be defined by the plugin loading system + get_icons = get_resources = None + +class InterfacePlugin(InterfaceAction): + + name = 'Interface Plugin Demo' + + # Declare the main action associated with this plugin + # The keyboard shortcut can be None if you dont want to use a keyboard + # shortcut. Remember that currently calibre has no central management for + # keyboard shortcuts, so try to use an unusual/unused shortcut. + action_spec = ('Interface Plugin Demo', None, + 'Run the Interface Plugin Demo', 'Ctrl+Shift+F1') + + def genesis(self): + # This method is called once per plugin, do initial setup here + + # Set the icon for this interface action + # The get_icons function is a builtin function defined for all your + # plugin code. It loads icons from the plugin zip file. It returns + # QIcon objects, if you want the actual data, use the analogous + # get_resources builtin function. + # + # Note that if you are loading more than one icon, for performance, you + # should pass a list of names to get_icons. In this case, get_icons + # will return a dictionary mapping names to QIcons. Names that + # are not found in the zip file will result in null QIcons. + icon = get_icons('images/icon.png') + + # The qaction is automatically created from the action_spec defined + # above + self.qaction.setIcon(icon) + self.qaction.triggered.connect(self.show_dialog) + + def show_dialog(self): + # self.gui is the main calibre GUI. It acts as the gateway to access + # all the elements of the calibre user interface, it should also be the + # parent of the dialog + d = DemoDialog(self.gui, self.qaction.icon()) + d.show() + +