From 6f09ab99762a4e5ece68afec6310c52163c24138 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 25 Mar 2011 19:01:10 -0600 Subject: [PATCH 1/4] version 0.7.52 --- Changelog.yaml | 6 ++++++ setup/installer/__init__.py | 2 +- src/calibre/constants.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Changelog.yaml b/Changelog.yaml index 8b29dc054c..8f47c24f21 100644 --- a/Changelog.yaml +++ b/Changelog.yaml @@ -19,6 +19,12 @@ # new recipes: # - title: +- version: 0.7.52 + date: 2011-03-25 + + bug fixes: + - title: "Fixes a typo in 0.7.51 that broke the downloading of some news. Apologies." + - version: 0.7.51 date: 2011-03-25 diff --git a/setup/installer/__init__.py b/setup/installer/__init__.py index f2d598e33a..79bb942cde 100644 --- a/setup/installer/__init__.py +++ b/setup/installer/__init__.py @@ -14,7 +14,7 @@ from setup.build_environment import HOST, PROJECT BASE_RSYNC = ['rsync', '-avz', '--delete'] EXCLUDES = [] for x in [ - 'src/calibre/plugins', 'src/calibre/manual', 'src/calibre/trac', 'recipes', + 'src/calibre/plugins', 'src/calibre/manual', 'src/calibre/trac', '.bzr', '.build', '.svn', 'build', 'dist', 'imgsrc', '*.pyc', '*.pyo', '*.swp', '*.swo', 'format_docs']: EXCLUDES.extend(['--exclude', x]) diff --git a/src/calibre/constants.py b/src/calibre/constants.py index 253e67cdd1..ff9895657d 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -2,7 +2,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' __appname__ = 'calibre' -__version__ = '0.7.51' +__version__ = '0.7.52' __author__ = "Kovid Goyal " import re, importlib From e8b3c586cd764d473c0056c547463a5072069bb1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 25 Mar 2011 20:04:54 -0600 Subject: [PATCH 2/4] IGN:Tag release --- Changelog.yaml | 1 + src/calibre/translations/calibre.pot | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Changelog.yaml b/Changelog.yaml index 8f47c24f21..2298912ecf 100644 --- a/Changelog.yaml +++ b/Changelog.yaml @@ -24,6 +24,7 @@ bug fixes: - title: "Fixes a typo in 0.7.51 that broke the downloading of some news. Apologies." + tickets: [742840] - version: 0.7.51 date: 2011-03-25 diff --git a/src/calibre/translations/calibre.pot b/src/calibre/translations/calibre.pot index 35aedb5c89..4c6f30f4d9 100644 --- a/src/calibre/translations/calibre.pot +++ b/src/calibre/translations/calibre.pot @@ -4,9 +4,9 @@ # msgid "" msgstr "" -"Project-Id-Version: calibre 0.7.51\n" -"POT-Creation-Date: 2011-03-25 12:23+MDT\n" -"PO-Revision-Date: 2011-03-25 12:23+MDT\n" +"Project-Id-Version: calibre 0.7.52\n" +"POT-Creation-Date: 2011-03-25 19:03+MDT\n" +"PO-Revision-Date: 2011-03-25 19:03+MDT\n" "Last-Translator: Automatically generated\n" "Language-Team: LANGUAGE\n" "MIME-Version: 1.0\n" From 842594ad918d172637c7ba73b38db055f2bf7ce8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 26 Mar 2011 08:36:17 -0600 Subject: [PATCH 3/4] Fix #742965 (Typo in News for 'Poughkeepsie Journal') --- recipes/poughkeepsie_journal.recipe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/poughkeepsie_journal.recipe b/recipes/poughkeepsie_journal.recipe index 2fa5c1951b..0a1a558e70 100644 --- a/recipes/poughkeepsie_journal.recipe +++ b/recipes/poughkeepsie_journal.recipe @@ -1,7 +1,7 @@ from calibre.web.feeds.news import BasicNewsRecipe class AdvancedUserRecipe1291143841(BasicNewsRecipe): - title = u'Poughkeepsipe Journal' + title = u'Poughkeepsie Journal' language = 'en' __author__ = 'weebl' oldest_article = 7 From 75a8f437703e819f659f8a92f6fdfd4dccc1e43a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 26 Mar 2011 13:06:47 -0600 Subject: [PATCH 4/4] ... --- src/calibre/customize/__init__.py | 15 ++- src/calibre/customize/ui.py | 59 ++--------- src/calibre/customize/zipplugin.py | 154 +++++++++++++++++++++++++++++ src/calibre/ebooks/txt/input.py | 2 - 4 files changed, 175 insertions(+), 55 deletions(-) create mode 100644 src/calibre/customize/zipplugin.py diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 67cf822a17..faa6fcada2 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -4,9 +4,22 @@ __copyright__ = '2008, Kovid Goyal ' import os, sys, zipfile, importlib -from calibre.constants import numeric_version +from calibre.constants import numeric_version, iswindows, isosx from calibre.ptempfile import PersistentTemporaryFile +platform = 'linux' +if iswindows: + platform = 'windows' +elif isosx: + platform = 'osx' + + +class PluginNotFound(ValueError): + pass + +class InvalidPlugin(ValueError): + pass + class Plugin(object): # {{{ ''' diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 0f5508a89e..9c8f80544b 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -2,17 +2,16 @@ from __future__ import with_statement __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -import os, shutil, traceback, functools, sys, re -from contextlib import closing +import os, shutil, traceback, functools, sys -from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \ - MetadataReaderPlugin, MetadataWriterPlugin, \ - InterfaceActionBase as InterfaceAction, \ - PreferencesPlugin +from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound, + MetadataReaderPlugin, MetadataWriterPlugin, + InterfaceActionBase as InterfaceAction, + PreferencesPlugin, platform, InvalidPlugin) from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin +from calibre.customize.zipplugin import loader from calibre.customize.profiles import InputProfile, OutputProfile from calibre.customize.builtins import plugins as builtin_plugins -from calibre.constants import numeric_version as version, iswindows, isosx from calibre.devices.interface import DevicePlugin from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata.covers import CoverDownload @@ -22,14 +21,6 @@ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \ from calibre.ebooks.epub.fix import ePubFixer from calibre.ebooks.metadata.sources.base import Source -platform = 'linux' -if iswindows: - platform = 'windows' -elif isosx: - platform = 'osx' - -from zipfile import ZipFile - def _config(): c = Config('customize') c.add_opt('plugins', default={}, help=_('Installed plugins')) @@ -42,11 +33,6 @@ def _config(): config = _config() -class InvalidPlugin(ValueError): - pass - -class PluginNotFound(ValueError): - pass def find_plugin(name): for plugin in _initialized_plugins: @@ -60,38 +46,7 @@ def load_plugin(path_to_zip_file): # {{{ :return: A :class:`Plugin` instance. ''' - #print 'Loading plugin from', path_to_zip_file - if not os.access(path_to_zip_file, os.R_OK): - raise PluginNotFound - with closing(ZipFile(path_to_zip_file)) as zf: - for name in zf.namelist(): - if name.lower().endswith('plugin.py'): - locals = {} - raw = zf.read(name) - lines, encoding = raw.splitlines(), 'utf-8' - cr = re.compile(r'coding[:=]\s*([-\w.]+)') - raw = [] - for l in lines[:2]: - match = cr.search(l) - if match is not None: - encoding = match.group(1) - else: - raw.append(l) - raw += lines[2:] - raw = '\n'.join(raw) - raw = raw.decode(encoding) - raw = re.sub('\r\n', '\n', raw) - exec raw in locals - for x in locals.values(): - if isinstance(x, type) and issubclass(x, Plugin) and \ - x.name != 'Trivial Plugin': - if x.minimum_calibre_version > version or \ - platform not in x.supported_platforms: - continue - - return x - - raise InvalidPlugin(_('No valid plugin found in ')+path_to_zip_file) + return loader.load(path_to_zip_file) # }}} diff --git a/src/calibre/customize/zipplugin.py b/src/calibre/customize/zipplugin.py new file mode 100644 index 0000000000..194f444207 --- /dev/null +++ b/src/calibre/customize/zipplugin.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os, zipfile, posixpath, importlib, threading, re +from collections import OrderedDict + +from calibre.customize import (Plugin, numeric_version, platform, + InvalidPlugin, PluginNotFound) + +# PEP 302 based plugin loading mechanism, works around the bug in zipimport in +# python 2.x that prevents importing from zip files in locations whose paths +# have non ASCII characters + + +class PluginLoader(object): + + ''' + The restrictions that a zip file must obey to be a valid calibre plugin + are: + + * The .py file that defines the main plugin class must have a name + that: + * Ends in plugin.py + * Is a valid python identifier (contains only English alphabets, + underscores and numbers and starts with an alphabet). This + applies to the file name minus the .py extension, obviously. + * Try to make this name as distinct as possible, as it will be + put into a global namespace of all plugins. + * The zip file must contain a .py file that defines the main plugin + class at the top level. That is, it must not be in a subdirectory. + The filename must follow the restrictions outlined above. + ''' + + def __init__(self): + self.loaded_plugins = {} + self._lock = threading.RLock() + self._identifier_pat = re.compile(r'[a-zA-Z][_0-9a-zA-Z]*') + + 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 + m = importlib.import_module( + 'calibre_plugins.%s.__init__'%plugin_name) + for obj in m.__dict__.itervalues(): + if isinstance(obj, type) and issubclass(obj, Plugin) and \ + obj.name != 'Trivial Plugin': + ans = obj + break + if ans is None: + raise InvalidPlugin('No plugin class found in %r:%r'%( + path_to_zip_file, plugin_name)) + + if ans.minimum_calibre_version < numeric_version: + raise InvalidPlugin( + 'The plugin at %r needs a version of calibre >= %r' % + (path_to_zip_file, '.'.join(ans.minimum_calibre_version))) + + if platform not in ans.supported_platforms: + raise InvalidPlugin( + 'The plugin at %r cannot be used on %s' % + (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, unicode) 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 plugin_name in self.loaded_plugins: + raise InvalidPlugin(( + 'The plugin in %r uses an import name %r that is already' + ' used by another plugin') % (path_to_zip_file, plugin_name)) + if self._identifier_pat.match(plugin_name) is None: + raise InvalidPlugin(( + 'The plugin at %r uses an invalid import name: %r' % + (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 names: + 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 list(names.iterkeys()): + 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() + + diff --git a/src/calibre/ebooks/txt/input.py b/src/calibre/ebooks/txt/input.py index 002f128392..99f7035800 100644 --- a/src/calibre/ebooks/txt/input.py +++ b/src/calibre/ebooks/txt/input.py @@ -165,8 +165,6 @@ class TXTInput(InputFormatPlugin): elif options.formatting_type == 'textile': log.debug('Running text through textile conversion...') html = convert_textile(txt) - # Textile input already runs smartypants - options.smarten_punctuation = False else: log.debug('Running text through basic conversion...') flow_size = getattr(options, 'flow_size', 0)