1
0
mirror of https://github.com/gryf/ebook-converter.git synced 2026-01-01 15:32:26 +01: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.ptempfile import PersistentTemporaryFile
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
platform = 'linux'
if iswindows:
platform = 'windows'
@@ -22,8 +21,8 @@ class InvalidPlugin(ValueError):
pass
class Plugin(object): # {{{
'''
class Plugin(object):
"""
A calibre plugin. Useful members include:
* ``self.plugin_path``: Stores path to the ZIP file that contains
@@ -43,23 +42,23 @@ class Plugin(object): # {{{
* :meth:`__enter__`
* :meth:`load_resources`
'''
"""
#: List of platforms this plugin works on.
#: For example: ``['windows', 'osx', 'linux']``
supported_platforms = []
#: The name of this plugin. You must set it something other
#: 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)
version = (1, 0, 0)
version = (1, 0, 0)
#: A short string describing what this plugin does
description = _('Does absolutely nothing')
description = _('Does absolutely nothing')
#: The author of this plugin
author = _('Unknown')
author = _('Unknown')
#: When more than one plugin exists for a filetype,
#: the plugins are run in order of decreasing priority.
@@ -80,11 +79,11 @@ class Plugin(object): # {{{
type = _('Base')
def __init__(self, plugin_path):
self.plugin_path = plugin_path
self.plugin_path = plugin_path
self.site_customization = None
def initialize(self):
'''
"""
Called once when calibre plugins are initialized. Plugins are
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
@@ -94,12 +93,13 @@ class Plugin(object): # {{{
resources from the plugin ZIP file. The path to the ZIP file is
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
def config_widget(self):
'''
"""
Implement this method and :meth:`save_settings` in your plugin to
use a custom configuration dialog, rather then relying on the simple
string based default customization.
@@ -113,98 +113,20 @@ class Plugin(object): # {{{
return a tuple of two strings (message, details), these will be
displayed as a warning dialog to the user and the process will be
aborted.
'''
"""
raise NotImplementedError()
def save_settings(self, config_widget):
'''
"""
Save the settings specified by the user with config_widget.
:param config_widget: The widget returned by :meth:`config_widget`.
'''
"""
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):
'''
"""
If this plugin comes in a ZIP file (user added plugin), this method
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'])
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
that were not found in the ZIP file will not be present in the
dictionary.
'''
"""
if self.plugin_path is None:
raise ValueError('This plugin was not loaded from a ZIP file')
ans = {}
@@ -231,7 +154,7 @@ class Plugin(object): # {{{
return ans
def customization_help(self, gui=False):
'''
"""
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.
@@ -246,18 +169,18 @@ class Plugin(object): # {{{
:param gui: If True return HTML help, otherwise return plain text help.
'''
"""
raise NotImplementedError()
def temporary_file(self, suffix):
'''
"""
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
be removed on interpreter shutdown. Use the ``name`` member of the
returned object to access the full path to the created temporary file.
:param suffix: The suffix that the temporary file will have.
'''
"""
return PersistentTemporaryFile(suffix)
def is_customizable(self):
@@ -268,17 +191,18 @@ class Plugin(object): # {{{
return False
def __enter__(self, *args):
'''
Add this plugin to the python path so that it's contents become directly importable.
Useful when bundling large python libraries into the plugin. Use it like this::
"""
Add this plugin to the python path so that it's contents become
directly importable. Useful when bundling large python libraries into
the plugin. Use it like this::
with plugin:
import something
'''
"""
if self.plugin_path is not None:
from ebook_converter.utils.zipfile import ZipFile
zf = ZipFile(self.plugin_path)
extensions = {x.rpartition('.')[-1].lower() for x in
zf.namelist()}
zf.namelist()}
zip_safe = True
for ext in ('pyd', 'so', 'dll', 'dylib'):
if ext in extensions:
@@ -290,52 +214,51 @@ class Plugin(object): # {{{
else:
from ebook_converter.ptempfile import TemporaryDirectory
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)
sys.path.insert(0, self.sys_insertion_path)
zf.close()
def __exit__(self, *args):
ip, it = getattr(self, 'sys_insertion_path', None), getattr(self,
'_sys_insertion_tdir', None)
ip = getattr(self, 'sys_insertion_path', None),
it = getattr(self, '_sys_insertion_tdir', None)
if ip in sys.path:
sys.path.remove(ip)
if hasattr(it, '__exit__'):
it.__exit__(*args)
def cli_main(self, args):
'''
"""
This method is the main entry point for your plugins command line
interface. It is called when the user does: calibre-debug -r "Plugin
Name". Any arguments passed are present in the args variable.
'''
"""
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.
'''
"""
#: Set of file types for which this plugin should be run.
#: Use '*' for all file types.
#: For example: ``{'lit', 'mobi', 'prc'}``
file_types = set()
file_types = set()
#: If True, this plugin is run when books are added
#: to the database
on_import = False
on_import = False
#: If True, this plugin is run after books are added
#: to the database. In this case the postimport and postadd
#: methods of the plugin are called.
on_postimport = False
on_postimport = False
#: If True, this plugin is run just before a conversion
on_preprocess = False
on_preprocess = False
#: If True, this plugin is run after conversion
#: on the final file produced by the conversion output plugin.
@@ -344,7 +267,7 @@ class FileTypePlugin(Plugin): # {{{
type = _('File type')
def run(self, path_to_ebook):
'''
"""
Run the plugin. Must be implemented in subclasses.
It should perform whatever modifications are required
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
it should raise an Exception. The default implementation
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
self.original_path_to_file).
the original file (before any file type plugins are run, is available
as self.original_path_to_file).
The modified e-book file should be created with the
:meth:`temporary_file` method.
@@ -361,55 +284,56 @@ class FileTypePlugin(Plugin): # {{{
:param path_to_ebook: Absolute path to the e-book.
:return: Absolute path to the modified e-book.
'''
"""
# Default implementation does nothing
return path_to_ebook
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
this is different from :meth:`postadd` which is called when the book record is created for
the first time. This method 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.
"""
Called post import, i.e., after the book file has been added to the
database. Note that this is different from :meth:`postadd` which is
called when the book record is created for the first time. This method
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_format: The file type of the book that was added.
:param db: Library database.
'''
"""
pass # Default implementation does nothing
def postadd(self, book_id, fmt_map, db):
'''
"""
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
has been added to a book. postadd() is called only when an entire book record
with possibly more than one book file has been created for the first time.
This is useful if you wish to modify the book record in the database when the
book is first added to calibre.
this is different from :meth:`postimport`, which is called after a
single book file has been added to a book. postadd() is called only
when an entire book record with possibly more than one book file has
been created for the first time. This is useful if you wish to modify
the book record in the database when the book is first added to
calibre.
:param book_id: Database id of the added book.
: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
existing file, as sometimes files are added as streams. In which case
it might be a dummy value or a non-existent path.
existing file, as sometimes files are added as streams. In which
case it might be a dummy value or a non-existent path.
:param db: Library database
'''
"""
pass # Default implementation does nothing
# }}}
class MetadataReaderPlugin(Plugin): # {{{
'''
class MetadataReaderPlugin(Plugin):
"""
A plugin that implements reading metadata from a set of file types.
'''
"""
#: Set of file types for which this plugin should be run.
#: For example: ``set(['lit', 'mobi', 'prc'])``
file_types = set()
file_types = set()
supported_platforms = ['windows', 'osx', 'linux']
version = numeric_version
author = 'Kovid Goyal'
author = 'Kovid Goyal'
type = _('Metadata reader')
@@ -418,30 +342,30 @@ class MetadataReaderPlugin(Plugin): # {{{
self.quick = False
def get_metadata(self, stream, type):
'''
"""
Return metadata for the file represented by stream (a file like object
that supports reading). Raise an exception when there is an error
with the input data.
:param type: The type of file. Guaranteed to be one of the entries
in :attr:`file_types`.
:return: A :class:`ebook_converter.ebooks.metadata.book.Metadata` object
'''
in :attr:`file_types`.
:return: A :class:`ebook_converter.ebooks.metadata.book.Metadata`
object
"""
return None
# }}}
class MetadataWriterPlugin(Plugin): # {{{
'''
class MetadataWriterPlugin(Plugin):
"""
A plugin that implements reading metadata from a set of file types.
'''
"""
#: Set of file types for which this plugin should be run.
#: For example: ``set(['lit', 'mobi', 'prc'])``
file_types = set()
file_types = set()
supported_platforms = ['windows', 'osx', 'linux']
version = numeric_version
author = 'Kovid Goyal'
author = 'Kovid Goyal'
type = _('Metadata writer')
@@ -450,24 +374,23 @@ class MetadataWriterPlugin(Plugin): # {{{
self.apply_null = False
def set_metadata(self, stream, mi, type):
'''
"""
Set metadata for the file represented by stream (a file like object
that supports reading). Raise an exception when there is an error
with the input data.
:param type: The type of file. Guaranteed to be one of the entries
in :attr:`file_types`.
:param mi: A :class:`ebook_converter.ebooks.metadata.book.Metadata` object
'''
in :attr:`file_types`.
:param mi: A :class:`ebook_converter.ebooks.metadata.book.Metadata`
object
"""
pass
# }}}
class CatalogPlugin(Plugin): # {{{
'''
class CatalogPlugin(Plugin):
"""
A plugin that implements a catalog generator.
'''
"""
resources_path = None
@@ -477,20 +400,24 @@ class CatalogPlugin(Plugin): # {{{
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
#: Option = namedtuple('Option', 'option, default, dest, help')
#: cli_options = [Option('--catalog-title', default = 'My Catalog',
#: dest = 'catalog_title', help = (_('Title of generated catalog. \nDefault:') + " '" + '%default' + "'"))]
#: cli_options parsed in ebook_converter.db.cli.cmd_catalog:option_parser()
#: dest = 'catalog_title', help = (_('Title of generated catalog. '
#: '\nDefault:') +
#: " '" + '%default' + "'"))]
#: cli_options parsed in
#: ebook_converter.db.cli.cmd_catalog:option_parser()
#:
cli_options = []
def _field_sorter(self, key):
'''
"""
Custom fields sort after standard fields
'''
"""
if key.startswith('#'):
return '~%s' % key[1:]
else:
@@ -507,10 +434,11 @@ class CatalogPlugin(Plugin): # {{{
def get_output_fields(self, db, opts):
# Return a list of requested fields
all_std_fields = {'author_sort','authors','comments','cover','formats',
'id','isbn','library_name','ondevice','pubdate','publisher',
'rating','series_index','series','size','tags','timestamp',
'title_sort','title','uuid','languages','identifiers'}
all_std_fields = {'author_sort', 'authors', 'comments', 'cover',
'formats', 'id', 'isbn', 'library_name', 'ondevice',
'pubdate', 'publisher', 'rating', 'series_index',
'series', 'size', 'tags', 'timestamp', 'title_sort',
'title', 'uuid', 'languages', 'identifiers'}
all_custom_fields = set(db.custom_field_keys())
for field in list(all_custom_fields):
fm = db.field_metadata[field]
@@ -527,48 +455,40 @@ class CatalogPlugin(Plugin): # {{{
if requested_fields - all_fields:
from ebook_converter.library import current_library_name
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" %
(current_library_name(), ', '.join(sorted(list(all_fields)))))
raise ValueError("unable to generate catalog with specified fields")
(current_library_name(),
', '.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]
else:
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')))
return fields
def initialize(self):
'''
"""
If plugin is not a built-in, copy the plugin's .ui and .py files from
the ZIP file to $TMPDIR.
Tab will be dynamically generated and added to the Catalog Options dialog in
ebook_converter.gui2.dialogs.catalog.py:Catalog
'''
Tab will be dynamically generated and added to the Catalog Options
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.ui import config
from ebook_converter.ptempfile import PersistentTemporaryDirectory
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()
if not type(self) in builtin_plugins:
raise ValueError(f'Plugin type "{self.__str__}" not found')
def run(self, path_to_output, opts, db, ids, notification=None):
'''
"""
Run the plugin. Must be implemented in subclasses.
It should generate the catalog in the format specified
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 opts: A dictionary of keyword arguments
:param db: A LibraryDatabase2 object
'''
"""
# Default implementation does nothing
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']
author = 'Kovid Goyal'
author = 'Kovid Goyal'
type = _('User interface action')
can_be_disabled = False
@@ -603,9 +521,9 @@ class InterfaceActionBase(Plugin): # {{{
self.actual_plugin_ = None
def load_actual_plugin(self, gui):
'''
"""
This method must return the actual interface action plugin object.
'''
"""
ac = self.actual_plugin_
if ac is None:
mod, cls = self.actual_plugin.split(':')
@@ -614,20 +532,18 @@ class InterfaceActionBase(Plugin): # {{{
self.actual_plugin_ = ac
return ac
# }}}
class PreferencesPlugin(Plugin):
class PreferencesPlugin(Plugin): # {{{
'''
"""
A plugin representing a widget displayed in the Preferences dialog.
This plugin has only one important method :meth:`create_widget`. The
various fields of the plugin control how it is categorized in the UI.
'''
"""
supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
author = 'Kovid Goyal'
type = _('Preferences')
can_be_disabled = False
@@ -636,7 +552,8 @@ class PreferencesPlugin(Plugin): # {{{
#: :meth:`create_widget`.
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
#: Where in the list of names in a category, the :attr:`gui_name` of this
@@ -659,14 +576,14 @@ class PreferencesPlugin(Plugin): # {{{
description = None
def create_widget(self, parent=None):
'''
"""
Create and return the actual Qt widget used for setting this group of
preferences. The widget must implement the
:class:`ebook_converter.gui2.preferences.ConfigWidgetInterface`.
The default implementation uses :attr:`config_widget` to instantiate
the widget.
'''
"""
base, _, wc = self.config_widget.partition(':')
if not wc:
wc = 'ConfigWidget'
@@ -674,20 +591,18 @@ class PreferencesPlugin(Plugin): # {{{
widget = getattr(base, wc)
return widget(parent)
# }}}
class StoreBase(Plugin): # {{{
class StoreBase(Plugin):
supported_platforms = ['windows', 'osx', 'linux']
author = 'John Schember'
author = 'John Schember'
type = _('Store')
# Information about the store. Should be in the primary language
# of the store. This should not be translatable when set by
# a subclass.
description = _('An e-book store.')
minimum_calibre_version = (0, 8, 0)
version = (1, 0, 1)
version = (1, 0, 1)
actual_plugin = None
@@ -702,11 +617,12 @@ class StoreBase(Plugin): # {{{
affiliate = False
def load_actual_plugin(self, gui):
'''
"""
This method must return the actual interface action plugin object.
'''
"""
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
def customization_help(self, gui=False):
@@ -724,35 +640,30 @@ class StoreBase(Plugin): # {{{
return self.actual_plugin_object.save_settings(config_widget)
raise NotImplementedError()
# }}}
class EditBookToolPlugin(Plugin): # {{{
class EditBookToolPlugin(Plugin):
type = _('Edit book tool')
minimum_calibre_version = (1, 46, 0)
# }}}
class LibraryClosedPlugin(Plugin): # {{{
'''
class LibraryClosedPlugin(Plugin):
"""
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.
At the moment these plugins won't be called by the CLI functions.
'''
"""
type = _('Library closed')
# minimum version 2.54 because that is when support was added
minimum_calibre_version = (2, 54, 0)
def run(self, db):
'''
"""
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
any signals.
'''
"""
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['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])