From 1a79146bf75377aa933509d1c672ec0971ec8247 Mon Sep 17 00:00:00 2001 From: gryf Date: Sun, 3 May 2020 09:55:22 +0200 Subject: [PATCH] Removed some more code --- ebook_converter/customize/__init__.py | 393 ++++++++++--------------- ebook_converter/customize/zipplugin.py | 205 ------------- 2 files changed, 152 insertions(+), 446 deletions(-) diff --git a/ebook_converter/customize/__init__.py b/ebook_converter/customize/__init__.py index bcc50ae..d5552cf 100644 --- a/ebook_converter/customize/__init__.py +++ b/ebook_converter/customize/__init__.py @@ -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 ' - 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') diff --git a/ebook_converter/customize/zipplugin.py b/ebook_converter/customize/zipplugin.py index 77ae2d0..8a542ca 100644 --- a/ebook_converter/customize/zipplugin.py +++ b/ebook_converter/customize/zipplugin.py @@ -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__ = "" - 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])