From a8065fd7150a2bbe85a051b1e226b94a6f8aa93b Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 20 Dec 2010 09:01:43 +0000 Subject: [PATCH 01/11] Make the title_sort order tweak respected in more places (hope, everywhere) --- src/calibre/ebooks/metadata/__init__.py | 2 ++ src/calibre/library/database2.py | 4 ++++ src/calibre/library/sqlite.py | 5 +---- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index 01e5190640..25127ee591 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -57,6 +57,8 @@ _ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x203 def title_sort(title): title = title.strip() + if tweaks['title_series_sorting'] == 'strictly_alphabetic': + return title if title and title[0] in _ignore_starts: title = title[1:] match = _title_pat.search(title) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 2848e8ebb3..cba49ae6ae 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1128,6 +1128,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): for l in list: (id, val, sort_val) = (l[0], l[1], l[2]) tids[category][val] = (id, sort_val) + elif cat['datatype'] == 'series': + for l in list: + (id, val) = (l[0], l[1]) + tids[category][val] = (id, title_sort(val)) elif cat['datatype'] == 'rating': for l in list: (id, val) = (l[0], l[1]) diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py index ca6b0fc178..521c275efe 100644 --- a/src/calibre/library/sqlite.py +++ b/src/calibre/library/sqlite.py @@ -161,10 +161,7 @@ class DBThread(Thread): self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate) self.conn.create_collation('PYNOCASE', partial(pynocase, encoding=encoding)) - if tweaks['title_series_sorting'] == 'strictly_alphabetic': - self.conn.create_function('title_sort', 1, lambda x:x) - else: - self.conn.create_function('title_sort', 1, title_sort) + self.conn.create_function('title_sort', 1, title_sort) self.conn.create_function('author_to_author_sort', 1, _author_to_author_sort) self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4())) From 81864723c683661abbb977162ad6cc8a9241aaf0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 20 Dec 2010 09:22:40 -0700 Subject: [PATCH 02/11] Fix #7981 (New recipe for Mish's Global Economic Trend Analysis) --- resources/recipes/ecotrend.recipe | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 resources/recipes/ecotrend.recipe diff --git a/resources/recipes/ecotrend.recipe b/resources/recipes/ecotrend.recipe new file mode 100644 index 0000000000..679f190e96 --- /dev/null +++ b/resources/recipes/ecotrend.recipe @@ -0,0 +1,42 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Darko Miletic ' +''' +globaleconomicanalysis.blogspot.com +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class GlobalEconomicAnalysis(BasicNewsRecipe): + title = "Mish's Global Economic Trend Analysis" + __author__ = 'Darko Miletic' + description = 'Thoughts on the global economy, housing, gold, silver, interest rates, oil, energy, China, commodities, the dollar, Euro, Renminbi, Yen, inflation, deflation, stagflation, precious metals, emerging markets, and policy decisions that affect the global markets.' + publisher = 'Mike Shedlock' + category = 'news, politics, economy, banking' + oldest_article = 7 + max_articles_per_feed = 200 + no_stylesheets = True + encoding = 'utf8' + use_embedded_content = True + language = 'en' + remove_empty_feeds = True + publication_type = 'blog' + masthead_url = 'http://www.pagina12.com.ar/commons/imgs/logo-home.gif' + extra_css = """ + body{font-family: Arial,Helvetica,sans-serif } + img{margin-bottom: 0.4em; display:block} + """ + + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } + + remove_tags = [ + dict(name=['meta','link','iframe','object','embed']) + ,dict(attrs={'class':'blogger-post-footer'}) + ] + remove_attributes=['border'] + + feeds = [(u'Articles', u'http://feeds2.feedburner.com/MishsGlobalEconomicTrendAnalysis')] From eac89c5439c99337dd94618f17cb5ab3827527bd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 20 Dec 2010 09:55:44 -0700 Subject: [PATCH 03/11] Linux binary build: If setting system default locale fails, try setting locale to en_US.UTF-8 instead --- setup/installer/linux/freeze2.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup/installer/linux/freeze2.py b/setup/installer/linux/freeze2.py index df2c1d6480..7a2980039e 100644 --- a/setup/installer/linux/freeze2.py +++ b/setup/installer/linux/freeze2.py @@ -318,7 +318,11 @@ class LinuxFreeze(Command): import codecs def set_default_encoding(): - locale.setlocale(locale.LC_ALL, '') + try: + locale.setlocale(locale.LC_ALL, '') + except: + print 'WARNING: Failed to set default libc locale, using en_US' + locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') enc = locale.getdefaultlocale()[1] if not enc: enc = locale.nl_langinfo(locale.CODESET) From 2719075963b5d502eba3378dde7d46d8a05ecaf0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 20 Dec 2010 10:07:37 -0700 Subject: [PATCH 04/11] Update Gazet van Antwerpen --- resources/recipes/gva_be.recipe | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/resources/recipes/gva_be.recipe b/resources/recipes/gva_be.recipe index 34c4122394..f42bd23417 100644 --- a/resources/recipes/gva_be.recipe +++ b/resources/recipes/gva_be.recipe @@ -40,13 +40,12 @@ class GazetvanAntwerpen(BasicNewsRecipe): remove_tags_after = dict(name='span', attrs={'class':'author'}) feeds = [ - (u'Overzicht & Blikvanger', u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/overview/overzicht' ) + (u'Binnenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/binnenland' ) + ,(u'Buitenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/buitenland' ) ,(u'Stad & Regio' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/stadenregio' ) ,(u'Economie' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/economie' ) - ,(u'Binnenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/binnenland' ) - ,(u'Buitenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/buitenland' ) ,(u'Media & Cultur' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/mediaencultuur') - ,(u'Wetenschap' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/mediaencultuur') + ,(u'Wetenschap' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/wetenschap' ) ,(u'Sport' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/sport' ) ] From 8ea848312c666cce6e6a71190aa4f834971a1ce1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 20 Dec 2010 10:52:44 -0700 Subject: [PATCH 05/11] Fix #7967 (Specific format actions show non-existent formats) --- setup/installer/linux/freeze2.py | 2 +- src/calibre/gui2/actions/delete.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/setup/installer/linux/freeze2.py b/setup/installer/linux/freeze2.py index 7a2980039e..bd8463b1a7 100644 --- a/setup/installer/linux/freeze2.py +++ b/setup/installer/linux/freeze2.py @@ -321,7 +321,7 @@ class LinuxFreeze(Command): try: locale.setlocale(locale.LC_ALL, '') except: - print 'WARNING: Failed to set default libc locale, using en_US' + print 'WARNING: Failed to set default libc locale, using en_US.UTF-8' locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') enc = locale.getdefaultlocale()[1] if not enc: diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index 24dd1d3e5c..27973b5f5b 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -97,10 +97,15 @@ class DeleteAction(InterfaceAction): for action in list(self.delete_menu.actions())[1:]: action.setEnabled(enabled) - def _get_selected_formats(self, msg): + def _get_selected_formats(self, msg, ids): from calibre.gui2.dialogs.select_formats import SelectFormats - fmts = self.gui.library_view.model().db.all_formats() - d = SelectFormats([x.lower() for x in fmts], msg, parent=self.gui) + fmts = set([]) + db = self.gui.library_view.model().db + for x in ids: + fmts_ = db.formats(x, index_is_id=True, verify_formats=False) + if fmts_: + fmts.update(frozenset([x.lower() for x in fmts_.split(',')])) + d = SelectFormats(list(sorted(fmts)), msg, parent=self.gui) if d.exec_() != d.Accepted: return None return d.selected_formats @@ -118,7 +123,7 @@ class DeleteAction(InterfaceAction): if not ids: return fmts = self._get_selected_formats( - _('Choose formats to be deleted')) + _('Choose formats to be deleted'), ids) if not fmts: return for id in ids: @@ -136,7 +141,7 @@ class DeleteAction(InterfaceAction): if not ids: return fmts = self._get_selected_formats( - '

'+_('Choose formats not to be deleted')) + '

'+_('Choose formats not to be deleted'), ids) if fmts is None: return for id in ids: From f4b7d64708312a8966cede28ccb24430a8456a19 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 20 Dec 2010 11:15:10 -0700 Subject: [PATCH 06/11] E-book viewer: SHow format of current book in the title bar. Fixes #7974 (Viewer - more info in title bar) --- src/calibre/gui2/viewer/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index f7b8a8d401..25f69b1558 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -650,7 +650,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.action_table_of_contents.setDisabled(not self.iterator.toc) self.current_book_has_toc = bool(self.iterator.toc) self.current_title = title - self.setWindowTitle(self.base_window_title+' - '+title) + self.setWindowTitle(self.base_window_title+' - '+title + + ' [%s]'%os.path.splitext(pathtoebook)[1][1:].upper()) self.pos.setMaximum(sum(self.iterator.pages)) self.pos.setSuffix(' / %d'%sum(self.iterator.pages)) self.vertical_scrollbar.setMinimum(100) From b4d7c5f7a6e8cba8166be30a5ec217e5b8c73084 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 20 Dec 2010 11:21:37 -0700 Subject: [PATCH 07/11] ... --- src/calibre/devices/usbms/device.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index af2948cf82..2c095d6f7b 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -605,8 +605,9 @@ class Device(DeviceConfig, DevicePlugin): main, carda, cardb = self.find_device_nodes() if main is None: - raise DeviceError(_('Unable to detect the %s disk drive. Your ' - ' kernel is probably exporting a deprecated version of SYSFS.') + raise DeviceError(_('Unable to detect the %s disk drive. Either ' + 'the device has already been ejected, or your ' + 'kernel is exporting a deprecated version of SYSFS.') %self.__class__.__name__) self._linux_mount_map = {} From a16708ca72c07bcf5d015440b0172e933f15638c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 20 Dec 2010 11:58:07 -0700 Subject: [PATCH 08/11] Implement #7947 (Bulk grab cover from format) --- src/calibre/gui2/dialogs/metadata_bulk.py | 47 ++++++++++++++++++++++- src/calibre/gui2/dialogs/metadata_bulk.ui | 11 +++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index e0f1f83c73..b875de14e3 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal ' '''Dialog to edit metadata in bulk''' -import re +import re, os from PyQt4.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \ pyqtSignal, QDialogButtonBox @@ -12,12 +12,41 @@ from PyQt4 import QtGui from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.ebooks.metadata import string_to_authors, authors_to_string +from calibre.ebooks.metadata.meta import get_metadata from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2 import error_dialog from calibre.gui2.progress_indicator import ProgressIndicator from calibre.utils.config import dynamic from calibre.utils.titlecase import titlecase from calibre.utils.icu import sort_key, capitalize +from calibre.utils.config import prefs +from calibre.utils.magick.draw import identify_data + +def get_cover_data(path): + old = prefs['read_file_metadata'] + if not old: + prefs['read_file_metadata'] = True + cdata = area = None + + try: + mi = get_metadata(open(path, 'rb'), + os.path.splitext(path)[1][1:].lower()) + if mi.cover and os.access(mi.cover, os.R_OK): + cdata = open(mi.cover).read() + elif mi.cover_data[1] is not None: + cdata = mi.cover_data[1] + if cdata: + width, height, fmt = identify_data(cdata) + area = width*height + except: + cdata = area = None + + if old != prefs['read_file_metadata']: + prefs['read_file_metadata'] = old + + return cdata, area + + class MyBlockingBusy(QDialog): @@ -146,6 +175,20 @@ class MyBlockingBusy(QDialog): cdata = calibre_cover(mi.title, mi.format_field('authors')[-1], series_string=series_string) self.db.set_cover(id, cdata) + elif cover_action == 'fromfmt': + fmts = self.db.formats(id, index_is_id=True, verify_formats=False) + if fmts: + covers = [] + for fmt in fmts.split(','): + fmt = self.db.format_abspath(id, fmt, index_is_id=True) + if not fmt: continue + cdata, area = get_cover_data(fmt) + if cdata: + covers.append((cdata, area)) + covers.sort(key=lambda x: x[1]) + if covers: + self.db.set_cover(id, covers[-1][0]) + covers = [] elif self.current_phase == 2: # All of these just affect the DB, so we can tolerate a total rollback if do_auto_author: @@ -700,6 +743,8 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): cover_action = 'remove' elif self.cover_generate.isChecked(): cover_action = 'generate' + elif self.cover_from_fmt.isChecked(): + cover_action = 'fromfmt' args = (remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, do_autonumber, do_remove_format, remove_format, do_swap_ta, diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index ecb34d8e5b..3f20958d47 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -414,6 +414,13 @@ Future conversion of these books will use the default settings. + + + + Set from &ebook file(s) + + + @@ -686,8 +693,8 @@ nothing should be put between the original text and the inserted text 0 0 - 122 - 38 + 726 + 334 From f595ff2dfc6b4b9177aa4c2248448336da0254e1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 20 Dec 2010 12:01:49 -0700 Subject: [PATCH 09/11] ... --- src/calibre/gui2/dialogs/metadata_bulk.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index b875de14e3..e5292ee755 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -185,10 +185,10 @@ class MyBlockingBusy(QDialog): cdata, area = get_cover_data(fmt) if cdata: covers.append((cdata, area)) - covers.sort(key=lambda x: x[1]) - if covers: - self.db.set_cover(id, covers[-1][0]) - covers = [] + covers.sort(key=lambda x: x[1]) + if covers: + self.db.set_cover(id, covers[-1][0]) + covers = [] elif self.current_phase == 2: # All of these just affect the DB, so we can tolerate a total rollback if do_auto_author: From 6e3d2db96c7958ccaaae78b82b665230931c3940 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 20 Dec 2010 13:20:21 -0700 Subject: [PATCH 10/11] On X11 initialize fontconfig in the GUI thread as Qt also uses fontconfig internally and fontconfig is not thread safe --- src/calibre/gui2/convert/mobi_output.py | 2 +- src/calibre/gui2/widgets.py | 2 +- src/calibre/utils/fonts/__init__.py | 19 +++++++++++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/convert/mobi_output.py b/src/calibre/gui2/convert/mobi_output.py index 14aca24db5..be9c5b4658 100644 --- a/src/calibre/gui2/convert/mobi_output.py +++ b/src/calibre/gui2/convert/mobi_output.py @@ -11,7 +11,6 @@ from PyQt4.Qt import Qt from calibre.gui2.convert.mobi_output_ui import Ui_Form from calibre.gui2.convert import Widget from calibre.gui2.widgets import FontFamilyModel -from calibre.utils.fonts import fontconfig font_family_model = None @@ -28,6 +27,7 @@ class PluginWidget(Widget, Ui_Form): 'mobi_ignore_margins', 'dont_compress', 'no_inline_toc', 'masthead_font','personal_doc'] ) + from calibre.utils.fonts import fontconfig self.db, self.book_id = db, book_id global font_family_model diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 12d64bbbcd..c5ae7fff85 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -19,7 +19,6 @@ from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs from calibre.constants import isosx from calibre.gui2.filename_pattern_ui import Ui_Form from calibre import fit_image -from calibre.utils.fonts import fontconfig from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks.metadata.meta import metadata_from_filename from calibre.utils.config import prefs, XMLConfig @@ -283,6 +282,7 @@ class FontFamilyModel(QAbstractListModel): def __init__(self, *args): QAbstractListModel.__init__(self, *args) + from calibre.utils.fonts import fontconfig try: self.families = fontconfig.find_font_families() except: diff --git a/src/calibre/utils/fonts/__init__.py b/src/calibre/utils/fonts/__init__.py index 5cab0c4920..3db3a1b285 100644 --- a/src/calibre/utils/fonts/__init__.py +++ b/src/calibre/utils/fonts/__init__.py @@ -7,15 +7,19 @@ __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os, sys -from threading import Thread -from calibre.constants import plugins, iswindows +from calibre.constants import plugins, iswindows, islinux, isfreebsd _fc, _fc_err = plugins['fontconfig'] if _fc is None: raise RuntimeError('Failed to load fontconfig with error:'+_fc_err) +if islinux or isfreebsd: + Thread = object +else: + from threading import Thread + class FontConfig(Thread): def __init__(self): @@ -45,7 +49,8 @@ class FontConfig(Thread): self.failed = True def wait(self): - self.join() + if not (islinux or isfreebsd): + self.join() if self.failed: raise RuntimeError('Failed to initialize fontconfig') @@ -144,7 +149,13 @@ class FontConfig(Thread): return fonts if all else (fonts[0] if fonts else None) fontconfig = FontConfig() -fontconfig.start() +if islinux or isfreebsd: + # On X11 Qt also uses fontconfig, so initialization must happen in the + # main thread. In any case on X11 initializing fontconfig should be very + # fast + fontconfig.run() +else: + fontconfig.start() def test(): from pprint import pprint; From cddf20727770508ffe1116c399a67403b67022a4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 20 Dec 2010 14:48:25 -0700 Subject: [PATCH 11/11] Fix #7980 (Security vulnerability in Calibre 0.7.34) --- src/calibre/library/server/content.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py index 5492c86fa9..8af70d5675 100644 --- a/src/calibre/library/server/content.py +++ b/src/calibre/library/server/content.py @@ -5,7 +5,7 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import re, os +import re, os, posixpath import cherrypy @@ -88,17 +88,24 @@ class ContentServer(object): def static(self, name): 'Serves static content' name = name.lower() - cherrypy.response.headers['Content-Type'] = { + fname = posixpath.basename(name) + try: + cherrypy.response.headers['Content-Type'] = { 'js' : 'text/javascript', 'css' : 'text/css', 'png' : 'image/png', 'gif' : 'image/gif', 'html' : 'text/html', - '' : 'application/octet-stream', - }[name.rpartition('.')[-1].lower()] + }[fname.rpartition('.')[-1].lower()] + except KeyError: + raise cherrypy.HTTPError(404, '%r not a valid resource type'%name) cherrypy.response.headers['Last-Modified'] = self.last_modified(self.build_time) - path = P('content_server/'+name) - if not os.path.exists(path): + basedir = os.path.abspath(P('content_server')) + path = os.path.join(basedir, name.replace('/', os.sep)) + path = os.path.abspath(path) + if not path.startswith(basedir): + raise cherrypy.HTTPError(403, 'Access to %s is forbidden'%name) + if not os.path.exists(path) or not os.path.isfile(path): raise cherrypy.HTTPError(404, '%s not found'%name) if self.opts.develop: lm = fromtimestamp(os.stat(path).st_mtime)