1
0
mirror of https://github.com/gryf/ebook-converter.git synced 2026-04-01 09:53:34 +02:00

Removed some more code

This commit is contained in:
2020-05-03 09:55:22 +02:00
parent 8f1eb9c88c
commit 1a79146bf7
2 changed files with 152 additions and 446 deletions

View File

@@ -1,12 +1,11 @@
import os, sys, zipfile, importlib import importlib
import sys
import zipfile
from ebook_converter.constants import numeric_version, iswindows, isosx from ebook_converter.constants import numeric_version, iswindows, isosx
from ebook_converter.ptempfile import PersistentTemporaryFile from ebook_converter.ptempfile import PersistentTemporaryFile
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
platform = 'linux' platform = 'linux'
if iswindows: if iswindows:
platform = 'windows' platform = 'windows'
@@ -22,8 +21,8 @@ class InvalidPlugin(ValueError):
pass pass
class Plugin(object): # {{{ class Plugin(object):
''' """
A calibre plugin. Useful members include: A calibre plugin. Useful members include:
* ``self.plugin_path``: Stores path to the ZIP file that contains * ``self.plugin_path``: Stores path to the ZIP file that contains
@@ -43,23 +42,23 @@ class Plugin(object): # {{{
* :meth:`__enter__` * :meth:`__enter__`
* :meth:`load_resources` * :meth:`load_resources`
''' """
#: List of platforms this plugin works on. #: List of platforms this plugin works on.
#: For example: ``['windows', 'osx', 'linux']`` #: For example: ``['windows', 'osx', 'linux']``
supported_platforms = [] supported_platforms = []
#: The name of this plugin. You must set it something other #: The name of this plugin. You must set it something other
#: than Trivial Plugin for it to work. #: than Trivial Plugin for it to work.
name = 'Trivial Plugin' name = 'Trivial Plugin'
#: The version of this plugin as a 3-tuple (major, minor, revision) #: The version of this plugin as a 3-tuple (major, minor, revision)
version = (1, 0, 0) version = (1, 0, 0)
#: A short string describing what this plugin does #: A short string describing what this plugin does
description = _('Does absolutely nothing') description = _('Does absolutely nothing')
#: The author of this plugin #: The author of this plugin
author = _('Unknown') author = _('Unknown')
#: When more than one plugin exists for a filetype, #: When more than one plugin exists for a filetype,
#: the plugins are run in order of decreasing priority. #: the plugins are run in order of decreasing priority.
@@ -80,11 +79,11 @@ class Plugin(object): # {{{
type = _('Base') type = _('Base')
def __init__(self, plugin_path): def __init__(self, plugin_path):
self.plugin_path = plugin_path self.plugin_path = plugin_path
self.site_customization = None self.site_customization = None
def initialize(self): def initialize(self):
''' """
Called once when calibre plugins are initialized. Plugins are Called once when calibre plugins are initialized. Plugins are
re-initialized every time a new plugin is added. Also note that if the re-initialized every time a new plugin is added. Also note that if the
plugin is run in a worker process, such as for adding books, then the plugin is run in a worker process, such as for adding books, then the
@@ -94,12 +93,13 @@ class Plugin(object): # {{{
resources from the plugin ZIP file. The path to the ZIP file is resources from the plugin ZIP file. The path to the ZIP file is
available as ``self.plugin_path``. available as ``self.plugin_path``.
Note that ``self.site_customization`` is **not** available at this point. Note that ``self.site_customization`` is **not** available at this
''' point.
"""
pass pass
def config_widget(self): def config_widget(self):
''' """
Implement this method and :meth:`save_settings` in your plugin to Implement this method and :meth:`save_settings` in your plugin to
use a custom configuration dialog, rather then relying on the simple use a custom configuration dialog, rather then relying on the simple
string based default customization. string based default customization.
@@ -113,98 +113,20 @@ class Plugin(object): # {{{
return a tuple of two strings (message, details), these will be return a tuple of two strings (message, details), these will be
displayed as a warning dialog to the user and the process will be displayed as a warning dialog to the user and the process will be
aborted. aborted.
''' """
raise NotImplementedError() raise NotImplementedError()
def save_settings(self, config_widget): def save_settings(self, config_widget):
''' """
Save the settings specified by the user with config_widget. Save the settings specified by the user with config_widget.
:param config_widget: The widget returned by :meth:`config_widget`. :param config_widget: The widget returned by :meth:`config_widget`.
''' """
raise NotImplementedError() raise NotImplementedError()
def do_user_config(self, parent=None):
'''
This method shows a configuration dialog for this plugin. It returns
True if the user clicks OK, False otherwise. The changes are
automatically applied.
'''
from PyQt5.Qt import QDialog, QDialogButtonBox, QVBoxLayout, \
QLabel, Qt, QLineEdit
from ebook_converter.gui2 import gprefs
prefname = 'plugin config dialog:'+self.type + ':' + self.name
geom = gprefs.get(prefname, None)
config_dialog = QDialog(parent)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
v = QVBoxLayout(config_dialog)
def size_dialog():
if geom is None:
config_dialog.resize(config_dialog.sizeHint())
else:
from PyQt5.Qt import QApplication
QApplication.instance().safe_restore_geometry(config_dialog, geom)
button_box.accepted.connect(config_dialog.accept)
button_box.rejected.connect(config_dialog.reject)
config_dialog.setWindowTitle(_('Customize') + ' ' + self.name)
try:
config_widget = self.config_widget()
except NotImplementedError:
config_widget = None
if isinstance(config_widget, tuple):
from ebook_converter.gui2 import warning_dialog
warning_dialog(parent, _('Cannot configure'), config_widget[0],
det_msg=config_widget[1], show=True)
return False
if config_widget is not None:
v.addWidget(config_widget)
v.addWidget(button_box)
size_dialog()
config_dialog.exec_()
if config_dialog.result() == QDialog.Accepted:
if hasattr(config_widget, 'validate'):
if config_widget.validate():
self.save_settings(config_widget)
else:
self.save_settings(config_widget)
else:
from ebook_converter.customize.ui import plugin_customization, \
customize_plugin
help_text = self.customization_help(gui=True)
help_text = QLabel(help_text, config_dialog)
help_text.setWordWrap(True)
help_text.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
help_text.setOpenExternalLinks(True)
v.addWidget(help_text)
sc = plugin_customization(self)
if not sc:
sc = ''
sc = sc.strip()
sc = QLineEdit(sc, config_dialog)
v.addWidget(sc)
v.addWidget(button_box)
size_dialog()
config_dialog.exec_()
if config_dialog.result() == QDialog.Accepted:
sc = str(sc.text()).strip()
customize_plugin(self, sc)
geom = bytearray(config_dialog.saveGeometry())
gprefs[prefname] = geom
return config_dialog.result()
def load_resources(self, names): def load_resources(self, names):
''' """
If this plugin comes in a ZIP file (user added plugin), this method If this plugin comes in a ZIP file (user added plugin), this method
will allow you to load resources from the ZIP file. will allow you to load resources from the ZIP file.
@@ -214,13 +136,14 @@ class Plugin(object): # {{{
pixmap.loadFromData(self.load_resources(['images/icon.png'])['images/icon.png']) pixmap.loadFromData(self.load_resources(['images/icon.png'])['images/icon.png'])
icon = QIcon(pixmap) icon = QIcon(pixmap)
:param names: List of paths to resources in the ZIP file using / as separator :param names: List of paths to resources in the ZIP file using / as
separator
:return: A dictionary of the form ``{name: file_contents}``. Any names :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 that were not found in the ZIP file will not be present in the
dictionary. dictionary.
''' """
if self.plugin_path is None: if self.plugin_path is None:
raise ValueError('This plugin was not loaded from a ZIP file') raise ValueError('This plugin was not loaded from a ZIP file')
ans = {} ans = {}
@@ -231,7 +154,7 @@ class Plugin(object): # {{{
return ans return ans
def customization_help(self, gui=False): def customization_help(self, gui=False):
''' """
Return a string giving help on how to customize this plugin. Return a string giving help on how to customize this plugin.
By default raise a :class:`NotImplementedError`, which indicates that By default raise a :class:`NotImplementedError`, which indicates that
the plugin does not require customization. the plugin does not require customization.
@@ -246,18 +169,18 @@ class Plugin(object): # {{{
:param gui: If True return HTML help, otherwise return plain text help. :param gui: If True return HTML help, otherwise return plain text help.
''' """
raise NotImplementedError() raise NotImplementedError()
def temporary_file(self, suffix): def temporary_file(self, suffix):
''' """
Return a file-like object that is a temporary file on the file system. Return a file-like object that is a temporary file on the file system.
This file will remain available even after being closed and will only This file will remain available even after being closed and will only
be removed on interpreter shutdown. Use the ``name`` member of the be removed on interpreter shutdown. Use the ``name`` member of the
returned object to access the full path to the created temporary file. returned object to access the full path to the created temporary file.
:param suffix: The suffix that the temporary file will have. :param suffix: The suffix that the temporary file will have.
''' """
return PersistentTemporaryFile(suffix) return PersistentTemporaryFile(suffix)
def is_customizable(self): def is_customizable(self):
@@ -268,17 +191,18 @@ class Plugin(object): # {{{
return False return False
def __enter__(self, *args): def __enter__(self, *args):
''' """
Add this plugin to the python path so that it's contents become directly importable. Add this plugin to the python path so that it's contents become
Useful when bundling large python libraries into the plugin. Use it like this:: directly importable. Useful when bundling large python libraries into
the plugin. Use it like this::
with plugin: with plugin:
import something import something
''' """
if self.plugin_path is not None: if self.plugin_path is not None:
from ebook_converter.utils.zipfile import ZipFile from ebook_converter.utils.zipfile import ZipFile
zf = ZipFile(self.plugin_path) zf = ZipFile(self.plugin_path)
extensions = {x.rpartition('.')[-1].lower() for x in extensions = {x.rpartition('.')[-1].lower() for x in
zf.namelist()} zf.namelist()}
zip_safe = True zip_safe = True
for ext in ('pyd', 'so', 'dll', 'dylib'): for ext in ('pyd', 'so', 'dll', 'dylib'):
if ext in extensions: if ext in extensions:
@@ -290,52 +214,51 @@ class Plugin(object): # {{{
else: else:
from ebook_converter.ptempfile import TemporaryDirectory from ebook_converter.ptempfile import TemporaryDirectory
self._sys_insertion_tdir = TemporaryDirectory('plugin_unzip') self._sys_insertion_tdir = TemporaryDirectory('plugin_unzip')
self.sys_insertion_path = self._sys_insertion_tdir.__enter__(*args) self.sys_insertion_path = (self._sys_insertion_tdir.
__enter__(*args))
zf.extractall(self.sys_insertion_path) zf.extractall(self.sys_insertion_path)
sys.path.insert(0, self.sys_insertion_path) sys.path.insert(0, self.sys_insertion_path)
zf.close() zf.close()
def __exit__(self, *args): def __exit__(self, *args):
ip, it = getattr(self, 'sys_insertion_path', None), getattr(self, ip = getattr(self, 'sys_insertion_path', None),
'_sys_insertion_tdir', None) it = getattr(self, '_sys_insertion_tdir', None)
if ip in sys.path: if ip in sys.path:
sys.path.remove(ip) sys.path.remove(ip)
if hasattr(it, '__exit__'): if hasattr(it, '__exit__'):
it.__exit__(*args) it.__exit__(*args)
def cli_main(self, args): def cli_main(self, args):
''' """
This method is the main entry point for your plugins command line This method is the main entry point for your plugins command line
interface. It is called when the user does: calibre-debug -r "Plugin interface. It is called when the user does: calibre-debug -r "Plugin
Name". Any arguments passed are present in the args variable. Name". Any arguments passed are present in the args variable.
''' """
raise NotImplementedError('The %s plugin has no command line interface' raise NotImplementedError('The %s plugin has no command line interface'
%self.name) % self.name)
# }}}
class FileTypePlugin(Plugin): # {{{ class FileTypePlugin(Plugin):
''' """
A plugin that is associated with a particular set of file types. A plugin that is associated with a particular set of file types.
''' """
#: Set of file types for which this plugin should be run. #: Set of file types for which this plugin should be run.
#: Use '*' for all file types. #: Use '*' for all file types.
#: For example: ``{'lit', 'mobi', 'prc'}`` #: For example: ``{'lit', 'mobi', 'prc'}``
file_types = set() file_types = set()
#: If True, this plugin is run when books are added #: If True, this plugin is run when books are added
#: to the database #: to the database
on_import = False on_import = False
#: If True, this plugin is run after books are added #: If True, this plugin is run after books are added
#: to the database. In this case the postimport and postadd #: to the database. In this case the postimport and postadd
#: methods of the plugin are called. #: methods of the plugin are called.
on_postimport = False on_postimport = False
#: If True, this plugin is run just before a conversion #: If True, this plugin is run just before a conversion
on_preprocess = False on_preprocess = False
#: If True, this plugin is run after conversion #: If True, this plugin is run after conversion
#: on the final file produced by the conversion output plugin. #: on the final file produced by the conversion output plugin.
@@ -344,7 +267,7 @@ class FileTypePlugin(Plugin): # {{{
type = _('File type') type = _('File type')
def run(self, path_to_ebook): def run(self, path_to_ebook):
''' """
Run the plugin. Must be implemented in subclasses. Run the plugin. Must be implemented in subclasses.
It should perform whatever modifications are required It should perform whatever modifications are required
on the e-book and return the absolute path to the on the e-book and return the absolute path to the
@@ -352,8 +275,8 @@ class FileTypePlugin(Plugin): # {{{
return the path to the original e-book. If an error is encountered return the path to the original e-book. If an error is encountered
it should raise an Exception. The default implementation it should raise an Exception. The default implementation
simply return the path to the original e-book. Note that the path to simply return the path to the original e-book. Note that the path to
the original file (before any file type plugins are run, is available as the original file (before any file type plugins are run, is available
self.original_path_to_file). as self.original_path_to_file).
The modified e-book file should be created with the The modified e-book file should be created with the
:meth:`temporary_file` method. :meth:`temporary_file` method.
@@ -361,55 +284,56 @@ class FileTypePlugin(Plugin): # {{{
:param path_to_ebook: Absolute path to the e-book. :param path_to_ebook: Absolute path to the e-book.
:return: Absolute path to the modified e-book. :return: Absolute path to the modified e-book.
''' """
# Default implementation does nothing # Default implementation does nothing
return path_to_ebook return path_to_ebook
def postimport(self, book_id, book_format, db): def postimport(self, book_id, book_format, db):
''' """
Called post import, i.e., after the book file has been added to the database. Note that Called post import, i.e., after the book file has been added to the
this is different from :meth:`postadd` which is called when the book record is created for database. Note that this is different from :meth:`postadd` which is
the first time. This method is called whenever a new file is added to a book record. It is called when the book record is created for the first time. This method
useful for modifying the book record based on the contents of the newly added file. is called whenever a new file is added to a book record. It is useful
for modifying the book record based on the contents of the newly added
file.
:param book_id: Database id of the added book. :param book_id: Database id of the added book.
:param book_format: The file type of the book that was added. :param book_format: The file type of the book that was added.
:param db: Library database. :param db: Library database.
''' """
pass # Default implementation does nothing pass # Default implementation does nothing
def postadd(self, book_id, fmt_map, db): def postadd(self, book_id, fmt_map, db):
''' """
Called post add, i.e. after a book has been added to the db. Note that Called post add, i.e. after a book has been added to the db. Note that
this is different from :meth:`postimport`, which is called after a single book file this is different from :meth:`postimport`, which is called after a
has been added to a book. postadd() is called only when an entire book record single book file has been added to a book. postadd() is called only
with possibly more than one book file has been created for the first time. when an entire book record with possibly more than one book file has
This is useful if you wish to modify the book record in the database when the been created for the first time. This is useful if you wish to modify
book is first added to calibre. the book record in the database when the book is first added to
calibre.
:param book_id: Database id of the added book. :param book_id: Database id of the added book.
:param fmt_map: Map of file format to path from which the file format :param fmt_map: Map of file format to path from which the file format
was added. Note that this might or might not point to an actual was added. Note that this might or might not point to an actual
existing file, as sometimes files are added as streams. In which case existing file, as sometimes files are added as streams. In which
it might be a dummy value or a non-existent path. case it might be a dummy value or a non-existent path.
:param db: Library database :param db: Library database
''' """
pass # Default implementation does nothing pass # Default implementation does nothing
# }}}
class MetadataReaderPlugin(Plugin):
class MetadataReaderPlugin(Plugin): # {{{ """
'''
A plugin that implements reading metadata from a set of file types. A plugin that implements reading metadata from a set of file types.
''' """
#: Set of file types for which this plugin should be run. #: Set of file types for which this plugin should be run.
#: For example: ``set(['lit', 'mobi', 'prc'])`` #: For example: ``set(['lit', 'mobi', 'prc'])``
file_types = set() file_types = set()
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
version = numeric_version version = numeric_version
author = 'Kovid Goyal' author = 'Kovid Goyal'
type = _('Metadata reader') type = _('Metadata reader')
@@ -418,30 +342,30 @@ class MetadataReaderPlugin(Plugin): # {{{
self.quick = False self.quick = False
def get_metadata(self, stream, type): def get_metadata(self, stream, type):
''' """
Return metadata for the file represented by stream (a file like object Return metadata for the file represented by stream (a file like object
that supports reading). Raise an exception when there is an error that supports reading). Raise an exception when there is an error
with the input data. with the input data.
:param type: The type of file. Guaranteed to be one of the entries :param type: The type of file. Guaranteed to be one of the entries
in :attr:`file_types`. in :attr:`file_types`.
:return: A :class:`ebook_converter.ebooks.metadata.book.Metadata` object :return: A :class:`ebook_converter.ebooks.metadata.book.Metadata`
''' object
"""
return None return None
# }}}
class MetadataWriterPlugin(Plugin): # {{{ class MetadataWriterPlugin(Plugin):
''' """
A plugin that implements reading metadata from a set of file types. A plugin that implements reading metadata from a set of file types.
''' """
#: Set of file types for which this plugin should be run. #: Set of file types for which this plugin should be run.
#: For example: ``set(['lit', 'mobi', 'prc'])`` #: For example: ``set(['lit', 'mobi', 'prc'])``
file_types = set() file_types = set()
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
version = numeric_version version = numeric_version
author = 'Kovid Goyal' author = 'Kovid Goyal'
type = _('Metadata writer') type = _('Metadata writer')
@@ -450,24 +374,23 @@ class MetadataWriterPlugin(Plugin): # {{{
self.apply_null = False self.apply_null = False
def set_metadata(self, stream, mi, type): def set_metadata(self, stream, mi, type):
''' """
Set metadata for the file represented by stream (a file like object Set metadata for the file represented by stream (a file like object
that supports reading). Raise an exception when there is an error that supports reading). Raise an exception when there is an error
with the input data. with the input data.
:param type: The type of file. Guaranteed to be one of the entries :param type: The type of file. Guaranteed to be one of the entries
in :attr:`file_types`. in :attr:`file_types`.
:param mi: A :class:`ebook_converter.ebooks.metadata.book.Metadata` object :param mi: A :class:`ebook_converter.ebooks.metadata.book.Metadata`
''' object
"""
pass pass
# }}}
class CatalogPlugin(Plugin):
class CatalogPlugin(Plugin): # {{{ """
'''
A plugin that implements a catalog generator. A plugin that implements a catalog generator.
''' """
resources_path = None resources_path = None
@@ -477,20 +400,24 @@ class CatalogPlugin(Plugin): # {{{
type = _('Catalog generator') type = _('Catalog generator')
#: CLI parser options specific to this plugin, declared as namedtuple Option: #: CLI parser options specific to this plugin, declared as namedtuple
#: Option:
#: #:
#: from collections import namedtuple #: from collections import namedtuple
#: Option = namedtuple('Option', 'option, default, dest, help') #: Option = namedtuple('Option', 'option, default, dest, help')
#: cli_options = [Option('--catalog-title', default = 'My Catalog', #: cli_options = [Option('--catalog-title', default = 'My Catalog',
#: dest = 'catalog_title', help = (_('Title of generated catalog. \nDefault:') + " '" + '%default' + "'"))] #: dest = 'catalog_title', help = (_('Title of generated catalog. '
#: cli_options parsed in ebook_converter.db.cli.cmd_catalog:option_parser() #: '\nDefault:') +
#: " '" + '%default' + "'"))]
#: cli_options parsed in
#: ebook_converter.db.cli.cmd_catalog:option_parser()
#: #:
cli_options = [] cli_options = []
def _field_sorter(self, key): def _field_sorter(self, key):
''' """
Custom fields sort after standard fields Custom fields sort after standard fields
''' """
if key.startswith('#'): if key.startswith('#'):
return '~%s' % key[1:] return '~%s' % key[1:]
else: else:
@@ -507,10 +434,11 @@ class CatalogPlugin(Plugin): # {{{
def get_output_fields(self, db, opts): def get_output_fields(self, db, opts):
# Return a list of requested fields # Return a list of requested fields
all_std_fields = {'author_sort','authors','comments','cover','formats', all_std_fields = {'author_sort', 'authors', 'comments', 'cover',
'id','isbn','library_name','ondevice','pubdate','publisher', 'formats', 'id', 'isbn', 'library_name', 'ondevice',
'rating','series_index','series','size','tags','timestamp', 'pubdate', 'publisher', 'rating', 'series_index',
'title_sort','title','uuid','languages','identifiers'} 'series', 'size', 'tags', 'timestamp', 'title_sort',
'title', 'uuid', 'languages', 'identifiers'}
all_custom_fields = set(db.custom_field_keys()) all_custom_fields = set(db.custom_field_keys())
for field in list(all_custom_fields): for field in list(all_custom_fields):
fm = db.field_metadata[field] fm = db.field_metadata[field]
@@ -527,48 +455,40 @@ class CatalogPlugin(Plugin): # {{{
if requested_fields - all_fields: if requested_fields - all_fields:
from ebook_converter.library import current_library_name from ebook_converter.library import current_library_name
invalid_fields = sorted(list(requested_fields - all_fields)) invalid_fields = sorted(list(requested_fields - all_fields))
print("invalid --fields specified: %s" % ', '.join(invalid_fields)) print("invalid --fields specified: %s" %
', '.join(invalid_fields))
print("available fields in '%s': %s" % print("available fields in '%s': %s" %
(current_library_name(), ', '.join(sorted(list(all_fields))))) (current_library_name(),
raise ValueError("unable to generate catalog with specified fields") ', '.join(sorted(list(all_fields)))))
raise ValueError("unable to generate catalog with specified "
"fields")
fields = [x for x in of if x in all_fields] fields = [x for x in of if x in all_fields]
else: else:
fields = sorted(all_fields, key=self._field_sorter) fields = sorted(all_fields, key=self._field_sorter)
if not opts.connected_device['is_device_connected'] and 'ondevice' in fields: if (not opts.connected_device['is_device_connected'] and
'ondevice' in fields):
fields.pop(int(fields.index('ondevice'))) fields.pop(int(fields.index('ondevice')))
return fields return fields
def initialize(self): def initialize(self):
''' """
If plugin is not a built-in, copy the plugin's .ui and .py files from If plugin is not a built-in, copy the plugin's .ui and .py files from
the ZIP file to $TMPDIR. the ZIP file to $TMPDIR.
Tab will be dynamically generated and added to the Catalog Options dialog in Tab will be dynamically generated and added to the Catalog Options
ebook_converter.gui2.dialogs.catalog.py:Catalog dialog in ebook_converter.gui2.dialogs.catalog.py:Catalog
''' """
# TODO(gryf): remove this entire abomination in favor of map and lazy
# importing if needed.
from ebook_converter.customize.builtins import plugins as builtin_plugins from ebook_converter.customize.builtins import plugins as builtin_plugins
from ebook_converter.customize.ui import config if not type(self) in builtin_plugins:
from ebook_converter.ptempfile import PersistentTemporaryDirectory raise ValueError(f'Plugin type "{self.__str__}" not found')
if not type(self) in builtin_plugins and self.name not in config['disabled_plugins']:
files_to_copy = ["%s.%s" % (self.name.lower(),ext) for ext in ["ui","py"]]
resources = zipfile.ZipFile(self.plugin_path,'r')
if self.resources_path is None:
self.resources_path = PersistentTemporaryDirectory('_plugin_resources', prefix='')
for file in files_to_copy:
try:
resources.extract(file, self.resources_path)
except:
print(" customize:__init__.initialize(): %s not found in %s" % (file, os.path.basename(self.plugin_path)))
continue
resources.close()
def run(self, path_to_output, opts, db, ids, notification=None): def run(self, path_to_output, opts, db, ids, notification=None):
''' """
Run the plugin. Must be implemented in subclasses. Run the plugin. Must be implemented in subclasses.
It should generate the catalog in the format specified It should generate the catalog in the format specified
in file_types, returning the absolute path to the in file_types, returning the absolute path to the
@@ -581,18 +501,16 @@ class CatalogPlugin(Plugin): # {{{
:param path_to_output: Absolute path to the generated catalog file. :param path_to_output: Absolute path to the generated catalog file.
:param opts: A dictionary of keyword arguments :param opts: A dictionary of keyword arguments
:param db: A LibraryDatabase2 object :param db: A LibraryDatabase2 object
''' """
# Default implementation does nothing # Default implementation does nothing
raise NotImplementedError('CatalogPlugin.generate_catalog() default ' raise NotImplementedError('CatalogPlugin.generate_catalog() default '
'method, should be overridden in subclass') 'method, should be overridden in subclass')
# }}}
class InterfaceActionBase(Plugin): # {{{ class InterfaceActionBase(Plugin):
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal' author = 'Kovid Goyal'
type = _('User interface action') type = _('User interface action')
can_be_disabled = False can_be_disabled = False
@@ -603,9 +521,9 @@ class InterfaceActionBase(Plugin): # {{{
self.actual_plugin_ = None self.actual_plugin_ = None
def load_actual_plugin(self, gui): def load_actual_plugin(self, gui):
''' """
This method must return the actual interface action plugin object. This method must return the actual interface action plugin object.
''' """
ac = self.actual_plugin_ ac = self.actual_plugin_
if ac is None: if ac is None:
mod, cls = self.actual_plugin.split(':') mod, cls = self.actual_plugin.split(':')
@@ -614,20 +532,18 @@ class InterfaceActionBase(Plugin): # {{{
self.actual_plugin_ = ac self.actual_plugin_ = ac
return ac return ac
# }}}
class PreferencesPlugin(Plugin):
class PreferencesPlugin(Plugin): # {{{ """
'''
A plugin representing a widget displayed in the Preferences dialog. A plugin representing a widget displayed in the Preferences dialog.
This plugin has only one important method :meth:`create_widget`. The This plugin has only one important method :meth:`create_widget`. The
various fields of the plugin control how it is categorized in the UI. various fields of the plugin control how it is categorized in the UI.
''' """
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal' author = 'Kovid Goyal'
type = _('Preferences') type = _('Preferences')
can_be_disabled = False can_be_disabled = False
@@ -636,7 +552,8 @@ class PreferencesPlugin(Plugin): # {{{
#: :meth:`create_widget`. #: :meth:`create_widget`.
config_widget = None config_widget = None
#: Where in the list of categories the :attr:`category` of this plugin should be. #: Where in the list of categories the :attr:`category` of this plugin
#: should be.
category_order = 100 category_order = 100
#: Where in the list of names in a category, the :attr:`gui_name` of this #: Where in the list of names in a category, the :attr:`gui_name` of this
@@ -659,14 +576,14 @@ class PreferencesPlugin(Plugin): # {{{
description = None description = None
def create_widget(self, parent=None): def create_widget(self, parent=None):
''' """
Create and return the actual Qt widget used for setting this group of Create and return the actual Qt widget used for setting this group of
preferences. The widget must implement the preferences. The widget must implement the
:class:`ebook_converter.gui2.preferences.ConfigWidgetInterface`. :class:`ebook_converter.gui2.preferences.ConfigWidgetInterface`.
The default implementation uses :attr:`config_widget` to instantiate The default implementation uses :attr:`config_widget` to instantiate
the widget. the widget.
''' """
base, _, wc = self.config_widget.partition(':') base, _, wc = self.config_widget.partition(':')
if not wc: if not wc:
wc = 'ConfigWidget' wc = 'ConfigWidget'
@@ -674,20 +591,18 @@ class PreferencesPlugin(Plugin): # {{{
widget = getattr(base, wc) widget = getattr(base, wc)
return widget(parent) return widget(parent)
# }}}
class StoreBase(Plugin):
class StoreBase(Plugin): # {{{
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
author = 'John Schember' author = 'John Schember'
type = _('Store') type = _('Store')
# Information about the store. Should be in the primary language # Information about the store. Should be in the primary language
# of the store. This should not be translatable when set by # of the store. This should not be translatable when set by
# a subclass. # a subclass.
description = _('An e-book store.') description = _('An e-book store.')
minimum_calibre_version = (0, 8, 0) minimum_calibre_version = (0, 8, 0)
version = (1, 0, 1) version = (1, 0, 1)
actual_plugin = None actual_plugin = None
@@ -702,11 +617,12 @@ class StoreBase(Plugin): # {{{
affiliate = False affiliate = False
def load_actual_plugin(self, gui): def load_actual_plugin(self, gui):
''' """
This method must return the actual interface action plugin object. This method must return the actual interface action plugin object.
''' """
mod, cls = self.actual_plugin.split(':') mod, cls = self.actual_plugin.split(':')
self.actual_plugin_object = getattr(importlib.import_module(mod), cls)(gui, self.name) self.actual_plugin_object = getattr(importlib.import_module(mod),
cls)(gui, self.name)
return self.actual_plugin_object return self.actual_plugin_object
def customization_help(self, gui=False): def customization_help(self, gui=False):
@@ -724,35 +640,30 @@ class StoreBase(Plugin): # {{{
return self.actual_plugin_object.save_settings(config_widget) return self.actual_plugin_object.save_settings(config_widget)
raise NotImplementedError() raise NotImplementedError()
# }}}
class EditBookToolPlugin(Plugin):
class EditBookToolPlugin(Plugin): # {{{
type = _('Edit book tool') type = _('Edit book tool')
minimum_calibre_version = (1, 46, 0) minimum_calibre_version = (1, 46, 0)
# }}}
class LibraryClosedPlugin(Plugin):
class LibraryClosedPlugin(Plugin): # {{{ """
'''
LibraryClosedPlugins are run when a library is closed, either at shutdown, LibraryClosedPlugins are run when a library is closed, either at shutdown,
when the library is changed, or when a library is used in some other way. when the library is changed, or when a library is used in some other way.
At the moment these plugins won't be called by the CLI functions. At the moment these plugins won't be called by the CLI functions.
''' """
type = _('Library closed') type = _('Library closed')
# minimum version 2.54 because that is when support was added # minimum version 2.54 because that is when support was added
minimum_calibre_version = (2, 54, 0) minimum_calibre_version = (2, 54, 0)
def run(self, db): def run(self, db):
''' """
The db will be a reference to the new_api (db.cache.py). The db will be a reference to the new_api (db.cache.py).
The plugin must run to completion. It must not use the GUI, threads, or The plugin must run to completion. It must not use the GUI, threads, or
any signals. any signals.
''' """
raise NotImplementedError('LibraryClosedPlugin ' raise NotImplementedError('LibraryClosedPlugin '
'run method must be overridden in subclass') 'run method must be overridden in subclass')
# }}}

View File

@@ -109,208 +109,3 @@ def load_translations(namespace, zfp):
namespace['_'] = getattr(trans, 'gettext') namespace['_'] = getattr(trans, 'gettext')
namespace['ngettext'] = getattr(trans, 'ngettext') namespace['ngettext'] = getattr(trans, 'ngettext')
class PluginLoader(object):
def __init__(self):
self.loaded_plugins = {}
self._lock = threading.RLock()
self._identifier_pat = re.compile(r'[a-zA-Z][_0-9a-zA-Z]*')
def _get_actual_fullname(self, fullname):
parts = fullname.split('.')
if parts[0] == 'calibre_plugins':
if len(parts) == 1:
return parts[0], None
plugin_name = parts[1]
with self._lock:
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__'
if fullname in names:
return fullname, plugin_name
if fullname+'.__init__' in names:
return fullname+'.__init__', plugin_name
return None, None
def find_module(self, fullname, path=None):
fullname, plugin_name = self._get_actual_fullname(fullname)
if fullname is None and plugin_name is None:
return None
return self
def load_module(self, fullname):
import_name, plugin_name = self._get_actual_fullname(fullname)
if import_name is None and plugin_name is None:
raise ImportError('No plugin named %r is loaded'%fullname)
mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
mod.__file__ = "<calibre Plugin Loader>"
mod.__loader__ = self
if import_name.endswith('.__init__') or import_name in ('__init__',
'calibre_plugins'):
# We have a package
mod.__path__ = []
if plugin_name is not None:
# We have some actual code to load
with self._lock:
zfp, names = self.loaded_plugins.get(plugin_name, (None, None))
if names is None:
raise ImportError('No plugin named %r loaded'%plugin_name)
zinfo = names.get(import_name, None)
if zinfo is None:
raise ImportError('Plugin %r has no module named %r' %
(plugin_name, import_name))
with zipfile.ZipFile(zfp) as zf:
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)
mod.__dict__['load_translations'] = partial(load_translations, mod.__dict__, zfp)
exec(compiled, mod.__dict__)
return mod
def load(self, path_to_zip_file):
if not os.access(path_to_zip_file, os.R_OK):
raise PluginNotFound('Cannot access %r'%path_to_zip_file)
with zipfile.ZipFile(path_to_zip_file) as zf:
plugin_name = self._locate_code(zf, path_to_zip_file)
try:
ans = None
plugin_module = 'calibre_plugins.%s'%plugin_name
m = sys.modules.get(plugin_module, None)
if m is not None:
reload(m)
else:
m = importlib.import_module(plugin_module)
plugin_classes = []
for obj in m.__dict__.values():
if isinstance(obj, type) and issubclass(obj, Plugin) and \
obj.name != 'Trivial Plugin':
plugin_classes.append(obj)
if not plugin_classes:
raise InvalidPlugin('No plugin class found in %s:%s'%(
as_unicode(path_to_zip_file), plugin_name))
if len(plugin_classes) > 1:
plugin_classes.sort(key=lambda c:(getattr(c, '__module__', None) or '').count('.'))
ans = plugin_classes[0]
if ans.minimum_calibre_version > numeric_version:
raise InvalidPlugin(
'The plugin at %s needs a version of calibre >= %s' %
(as_unicode(path_to_zip_file), '.'.join(map(str,
ans.minimum_calibre_version))))
if platform not in ans.supported_platforms:
raise InvalidPlugin(
'The plugin at %s cannot be used on %s' %
(as_unicode(path_to_zip_file), platform))
return ans
except:
with self._lock:
del self.loaded_plugins[plugin_name]
raise
def _locate_code(self, zf, path_to_zip_file):
names = [x if isinstance(x, str) else x.decode('utf-8') for x in
zf.namelist()]
names = [x[1:] if x[0] == '/' else x for x in names]
plugin_name = None
for name in names:
name, ext = posixpath.splitext(name)
if name.startswith('plugin-import-name-') and ext == '.txt':
plugin_name = name.rpartition('-')[-1]
if plugin_name is None:
c = 0
while True:
c += 1
plugin_name = 'dummy%d'%c
if plugin_name not in self.loaded_plugins:
break
else:
if self._identifier_pat.match(plugin_name) is None:
raise InvalidPlugin((
'The plugin at %r uses an invalid import name: %r' %
(path_to_zip_file, plugin_name)))
pynames = [x for x in names if x.endswith('.py')]
candidates = [posixpath.dirname(x) for x in pynames if
x.endswith('/__init__.py')]
candidates.sort(key=lambda x: x.count('/'))
valid_packages = set()
for candidate in candidates:
parts = candidate.split('/')
parent = '.'.join(parts[:-1])
if parent and parent not in valid_packages:
continue
valid_packages.add('.'.join(parts))
names = OrderedDict()
for candidate in pynames:
parts = posixpath.splitext(candidate)[0].split('/')
package = '.'.join(parts[:-1])
if package and package not in valid_packages:
continue
name = '.'.join(parts)
names[name] = zf.getinfo(candidate)
# Legacy plugins
if '__init__' not in names:
for name in tuple(names):
if '.' not in name and name.endswith('plugin'):
names['__init__'] = names[name]
break
if '__init__' not in names:
raise InvalidPlugin(('The plugin in %r is invalid. It does not '
'contain a top-level __init__.py file')
% path_to_zip_file)
with self._lock:
self.loaded_plugins[plugin_name] = (path_to_zip_file, names)
return plugin_name
loader = PluginLoader()
#sys.meta_path.insert(0, loader)
if __name__ == '__main__':
from tempfile import NamedTemporaryFile
from ebook_converter.customize.ui import add_plugin
from ebook_converter 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)
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])