From 8a3cb9b977abf1b0017fb83694df40d75e032d19 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Fri, 12 Jul 2013 17:22:45 +0200 Subject: [PATCH 1/6] Add ability to create a composite column containing the virtual libraries that the book is a member of. Intermediate commit -- still testing --- src/calibre/gui2/search_restriction_mixin.py | 7 ++ src/calibre/library/caches.py | 115 ++++++++++++------- src/calibre/library/cli.py | 2 +- src/calibre/library/database2.py | 6 + src/calibre/library/field_metadata.py | 10 ++ src/calibre/utils/formatter_functions.py | 12 +- src/calibre/utils/search_query_parser.py | 15 ++- 7 files changed, 119 insertions(+), 48 deletions(-) diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index b986a2a78e..a90d607ea9 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -332,6 +332,7 @@ class SearchRestrictionMixin(object): virt_libs = db.prefs.get('virtual_libraries', {}) virt_libs[name] = search db.prefs.set('virtual_libraries', virt_libs) + db.data.invalidate_virtual_libraries_caches(db) def do_create_edit(self, name=None): db = self.library_view.model().db @@ -341,8 +342,11 @@ class SearchRestrictionMixin(object): if name: self._remove_vl(name, reapply=False) self.add_virtual_library(db, cd.library_name, cd.library_search) + db.data.invalidate_virtual_libraries_caches(db) if not name or name == db.data.get_base_restriction_name(): self.apply_virtual_library(cd.library_name) + else: + self.tags_view.recount() def virtual_library_clicked(self): m = self.virtual_library_menu @@ -462,6 +466,9 @@ class SearchRestrictionMixin(object): default_yes=False): return self._remove_vl(name, reapply=True) + db = self.library_view.model().db + db.data.invalidate_virtual_libraries_caches(db) + self.tags_view.recount() def _remove_vl(self, name, reapply=True): db = self.library_view.model().db diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index e552ead591..05d964be71 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -2,7 +2,7 @@ # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import with_statement -__license__ = 'GPL v3' +__license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' @@ -144,7 +144,8 @@ def force_to_bool(val): class CacheRow(list): # {{{ - def __init__(self, db, composites, val, series_col, series_sort_col): + def __init__(self, db, composites, val, series_col, series_sort_col, + virtual_library_col): self.db = db self._composites = composites list.__init__(self, val) @@ -152,6 +153,8 @@ class CacheRow(list): # {{{ self._series_col = series_col self._series_sort_col = series_sort_col self._series_sort = None + self._virt_lib_col = virtual_library_col + self._virt_libs = None def __getitem__(self, col): if self._must_do: @@ -171,7 +174,7 @@ class CacheRow(list): # {{{ mi = self.db.get_metadata(id_, index_is_id=True, get_user_categories=False) for c in self._composites: - self[c] = mi.get(self._composites[c]) + self[c] = mi.get(self._composites[c]) if col == self._series_sort_col and self._series_sort is None: if self[self._series_col]: self._series_sort = title_sort(self[self._series_col]) @@ -179,6 +182,26 @@ class CacheRow(list): # {{{ else: self._series_sort = '' self[self._series_sort_col] = '' + + if col == self._virt_lib_col and self._virt_libs is None: + try: + if not getattr(self.db.data, '_virt_libs_computed', False): + self.db.data._ids_in_virt_libs = {} + for v,s in self.db.prefs.get('virtual_libraries', {}).iteritems(): + self.db.data._ids_in_virt_libs[v] = self.db.data.search_raw(s) + self.db.data._virt_libs_computed = True + r = [] + for v in self.db.prefs.get('virtual_libraries', {}).keys(): + # optimize the lookup of the ID -- it is always zero + if self[0] in self.db.data._ids_in_virt_libs[v]: + r.append(v) + from calibre.utils.icu import sort_key + self._virt_libs = ", ".join(sorted(r, key=sort_key)) + self[self._virt_lib_col] = self._virt_libs + except: + print len(self) + traceback.print_exc() + return list.__getitem__(self, col) def __getslice__(self, i, j): @@ -186,7 +209,7 @@ class CacheRow(list): # {{{ def refresh_composites(self): for c in self._composites: - self[c] = None + self[c] = None self._must_do = True # }}} @@ -206,6 +229,7 @@ class ResultCache(SearchQueryParser): # {{{ self.composites[field_metadata[key]['rec_index']] = key self.series_col = field_metadata['series']['rec_index'] self.series_sort_col = field_metadata['series_sort']['rec_index'] + self.virtual_libraries_col = field_metadata['virtual_libraries']['rec_index'] self._data = [] self._map = self._map_filtered = [] self.first_sort = True @@ -312,12 +336,12 @@ class ResultCache(SearchQueryParser): # {{{ '<=':[2, relop_le] } - local_today = ('_today', icu_lower(_('today'))) - local_yesterday = ('_yesterday', icu_lower(_('yesterday'))) - local_thismonth = ('_thismonth', icu_lower(_('thismonth'))) - local_daysago = icu_lower(_('daysago')) - local_daysago_len = len(local_daysago) - untrans_daysago = '_daysago' + local_today = ('_today', icu_lower(_('today'))) + local_yesterday = ('_yesterday', icu_lower(_('yesterday'))) + local_thismonth = ('_thismonth', icu_lower(_('thismonth'))) + local_daysago = icu_lower(_('daysago')) + local_daysago_len = len(local_daysago) + untrans_daysago = '_daysago' untrans_daysago_len = len('_daysago') def get_dates_matches(self, location, query, candidates): @@ -413,21 +437,21 @@ class ResultCache(SearchQueryParser): # {{{ if val_func is None: loc = self.field_metadata[location]['rec_index'] - val_func = lambda item, loc=loc: item[loc] + val_func = lambda item, loc = loc: item[loc] q = '' cast = adjust = lambda x: x dt = self.field_metadata[location]['datatype'] if query == 'false': if dt == 'rating' or location == 'cover': - relop = lambda x,y: not bool(x) + relop = lambda x, y: not bool(x) else: - relop = lambda x,y: x is None + relop = lambda x, y: x is None elif query == 'true': if dt == 'rating' or location == 'cover': - relop = lambda x,y: bool(x) + relop = lambda x, y: bool(x) else: - relop = lambda x,y: x is not None + relop = lambda x, y: x is not None else: relop = None for k in self.numeric_search_relops.keys(): @@ -441,7 +465,7 @@ class ResultCache(SearchQueryParser): # {{{ cast = lambda x: int(x) elif dt == 'rating': cast = lambda x: 0 if x is None else int(x) - adjust = lambda x: x/2 + adjust = lambda x: x / 2 elif dt in ('float', 'composite'): cast = lambda x : float(x) else: # count operation @@ -449,7 +473,7 @@ class ResultCache(SearchQueryParser): # {{{ if len(query) > 1: mult = query[-1:].lower() - mult = {'k':1024.,'m': 1024.**2, 'g': 1024.**3}.get(mult, 1.0) + mult = {'k':1024., 'm': 1024.**2, 'g': 1024.**3}.get(mult, 1.0) if mult != 1.0: query = query[:-1] else: @@ -568,12 +592,12 @@ class ResultCache(SearchQueryParser): # {{{ query = icu_lower(query) return matchkind, query - local_no = icu_lower(_('no')) - local_yes = icu_lower(_('yes')) + local_no = icu_lower(_('no')) + local_yes = icu_lower(_('yes')) local_unchecked = icu_lower(_('unchecked')) - local_checked = icu_lower(_('checked')) - local_empty = icu_lower(_('empty')) - local_blank = icu_lower(_('blank')) + local_checked = icu_lower(_('checked')) + local_empty = icu_lower(_('empty')) + local_blank = icu_lower(_('blank')) local_bool_values = ( local_no, local_unchecked, '_no', 'false', local_yes, local_checked, '_yes', 'true', @@ -696,8 +720,8 @@ class ResultCache(SearchQueryParser): # {{{ if fm['is_multiple'] and \ len(query) > 1 and query.startswith('#') and \ query[1:1] in '=<>!': - vf = lambda item, loc=fm['rec_index'], \ - ms=fm['is_multiple']['cache_to_list']:\ + vf = lambda item, loc = fm['rec_index'], \ + ms = fm['is_multiple']['cache_to_list']:\ len(item[loc].split(ms)) if item[loc] is not None else 0 return self.get_numeric_matches(location, query[1:], candidates, val_func=vf) @@ -707,7 +731,7 @@ class ResultCache(SearchQueryParser): # {{{ if fm.get('is_csp', False): if location == 'identifiers' and original_location == 'isbn': return self.get_keypair_matches('identifiers', - '=isbn:'+query, candidates) + '=isbn:' + query, candidates) return self.get_keypair_matches(location, query, candidates) # check for user categories @@ -759,7 +783,7 @@ class ResultCache(SearchQueryParser): # {{{ q = canonicalize_lang(query) if q is None: lm = lang_map() - rm = {v.lower():k for k,v in lm.iteritems()} + rm = {v.lower():k for k, v in lm.iteritems()} q = rm.get(query, query) else: q = query @@ -772,7 +796,7 @@ class ResultCache(SearchQueryParser): # {{{ if not item[loc]: if q == 'false' and matchkind == CONTAINS_MATCH: matches.add(item[0]) - continue # item is empty. No possible matches below + continue # item is empty. No possible matches below if q == 'false'and matchkind == CONTAINS_MATCH: # Field has something in it, so a false query does not match continue @@ -816,6 +840,13 @@ class ResultCache(SearchQueryParser): # {{{ current_candidates -= matches return matches + def invalidate_virtual_libraries_caches(self, db): + self.refresh(db) + + def search_raw(self, query): + matches = self.parse(query) + return matches + def search(self, query, return_matches=False): ans = self.search_getting_ids(query, self.search_restriction, set_restriction_count=True) @@ -973,10 +1004,11 @@ class ResultCache(SearchQueryParser): # {{{ try: self._data[id] = CacheRow(db, self.composites, db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0], - self.series_col, self.series_sort_col) + self.series_col, self.series_sort_col, + self.virtual_libraries_col) self._data[id].append(db.book_on_device_string(id)) - self._data[id].append(self.marked_ids_dict.get(id, None)) - self._data[id].append(None) + self._data[id].extend((self.marked_ids_dict.get(id, None), None, None)) + self._virt_libs_computed = False self._uuid_map[self._data[id][self._uuid_column_index]] = id except IndexError: return None @@ -989,14 +1021,15 @@ class ResultCache(SearchQueryParser): # {{{ def books_added(self, ids, db): if not ids: return - self._data.extend(repeat(None, max(ids)-len(self._data)+2)) + self._data.extend(repeat(None, max(ids) - len(self._data) + 2)) for id in ids: self._data[id] = CacheRow(db, self.composites, db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0], - self.series_col, self.series_sort_col) + self.series_col, self.series_sort_col, + self.virtual_libraries_col) self._data[id].append(db.book_on_device_string(id)) - self._data[id].append(self.marked_ids_dict.get(id, None)) - self._data[id].append(None) # Series sort column + self._data[id].extend((self.marked_ids_dict.get(id, None), None, None)) + self._virt_libs_computed = False self._uuid_map[self._data[id][self._uuid_column_index]] = id self._map[0:0] = ids self._map_filtered[0:0] = ids @@ -1020,20 +1053,22 @@ class ResultCache(SearchQueryParser): # {{{ db.initialize_template_cache() temp = db.conn.get('SELECT * FROM meta2') - self._data = list(itertools.repeat(None, temp[-1][0]+2)) if temp else [] + self._data = list(itertools.repeat(None, temp[-1][0] + 2)) if temp else [] for r in temp: self._data[r[0]] = CacheRow(db, self.composites, r, - self.series_col, self.series_sort_col) + self.series_col, self.series_sort_col, + self.virtual_libraries_col) self._uuid_map[self._data[r[0]][self._uuid_column_index]] = r[0] for item in self._data: if item is not None: item.append(db.book_on_device_string(item[0])) - # Temp mark and series_sort columns - item.extend((None, None)) + # Temp mark, series_sort, virtual_library columns + item.extend((None, None, None)) + self._virt_libs_computed = False marked_col = self.FIELD_MAP['marked'] - for id_,val in self.marked_ids_dict.iteritems(): + for id_, val in self.marked_ids_dict.iteritems(): try: self._data[id_][marked_col] = val except: @@ -1134,7 +1169,7 @@ class SortKeyGenerator(object): for i, candidate in enumerate( ('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')): if val.endswith(candidate): - p = 1024**(i) + p = 1024 ** (i) val = val[:-len(candidate)].strip() break val = locale.atof(val) * p diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 547cc5bc08..a86ec7ef6d 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -575,7 +575,7 @@ def command_set_metadata(args, dbpath): for key in sorted(db.field_metadata.all_field_keys()): m = db.field_metadata[key] if (key not in {'formats', 'series_sort', 'ondevice', 'path', - 'last_modified'} and m['is_editable'] and m['name']): + 'virtual_libraries', 'last_modified'} and m['is_editable'] and m['name']): yield key, m if m['datatype'] == 'series': si = m.copy() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index bd3f155c9d..d23633e433 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -449,6 +449,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.field_metadata.set_field_record_index('marked', base, prefer_custom=False) self.FIELD_MAP['series_sort'] = base = base+1 self.field_metadata.set_field_record_index('series_sort', base, prefer_custom=False) + self.FIELD_MAP['virtual_libraries'] = base = base+1 + self.field_metadata.set_field_record_index('virtual_libraries', base, prefer_custom=False) script = ''' DROP VIEW IF EXISTS meta2; @@ -992,6 +994,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): mi.book_size = row[fm['size']] mi.ondevice_col= row[fm['ondevice']] mi.last_modified = row[fm['last_modified']] + + mi._base_db_row = row # So the formatter functions can see the underlying data + mi._virt_lib_column = fm['virtual_libraries'] + formats = row[fm['formats']] mi.format_metadata = {} if not formats: diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 08c26e95a9..a96f819b58 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -387,6 +387,16 @@ class FieldMetadata(dict): 'is_custom':False, 'is_category':False, 'is_csp': False}), + ('virtual_libraries', {'table':None, + 'column':None, + 'datatype':'text', + 'is_multiple':{}, + 'kind':'field', + 'name':_('Virtual Libraries'), + 'search_terms':['virtual_libraries'], + 'is_custom':False, + 'is_category':False, + 'is_csp': False}), ] # }}} diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 73dad7422b..c94467fec0 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -1209,9 +1209,19 @@ class BuiltinFinishFormatting(BuiltinFormatterFunction): return val return prefix + formatter._do_format(val, fmt) + suffix +class BuiltinBookInVirtualLibraries(BuiltinFormatterFunction): + name = 'book_in_virtual_libraries' + arg_count = 0 + category = 'Get values from metadata' + __doc__ = doc = _('book_in_virtual_libraries() -- returns a list of ' + 'virtual libraries that this book is in.') + + def evaluate(self, formatter, kwargs, mi, locals_): + return mi._base_db_row[mi._virt_lib_column ] + _formatter_builtins = [ BuiltinAdd(), BuiltinAnd(), BuiltinApproximateFormats(), - BuiltinAssign(), BuiltinBooksize(), + BuiltinAssign(), BuiltinBookInVirtualLibraries(), BuiltinBooksize(), BuiltinCapitalize(), BuiltinCmp(), BuiltinContains(), BuiltinCount(), BuiltinCurrentLibraryName(), BuiltinCurrentLibraryPath(), BuiltinDaysBetween(), BuiltinDivide(), BuiltinEval(), BuiltinFirstNonEmpty(), diff --git a/src/calibre/utils/search_query_parser.py b/src/calibre/utils/search_query_parser.py index 2682088681..08a70a533d 100644 --- a/src/calibre/utils/search_query_parser.py +++ b/src/calibre/utils/search_query_parser.py @@ -294,6 +294,7 @@ class SearchQueryParser(object): def __init__(self, locations, test=False, optimize=False): self.sqp_initialize(locations, test=test, optimize=optimize) + self.sqp_parsed_search_cache = {} self.parser = Parser() def sqp_change_locations(self, locations): @@ -308,8 +309,7 @@ class SearchQueryParser(object): # empty the list of searches used for recursion testing self.recurse_level = 0 self.searches_seen = set([]) - candidates = self.universal_set() - return self._parse(query, candidates) + return self._parse(query) # this parse is used internally because it doesn't clear the # recursive search test list. However, we permit seeing the @@ -317,10 +317,13 @@ class SearchQueryParser(object): # another search. def _parse(self, query, candidates=None): self.recurse_level += 1 - try: - res = self.parser.parse(query, self.locations) - except RuntimeError: - raise ParseException(_('Failed to parse query, recursion limit reached: %s')%repr(query)) + res = self.sqp_parsed_search_cache.get(query, None) + if res is None: + try: + res = self.parser.parse(query, self.locations) + self.sqp_parsed_search_cache[query] = res + except RuntimeError: + raise ParseException(_('Failed to parse query, recursion limit reached: %s')%repr(query)) if candidates is None: candidates = self.universal_set() t = self.evaluate(res, candidates) From 090eed4153772a7a6cbcdf67ddb321eae324f57c Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sat, 13 Jul 2013 11:09:14 +0200 Subject: [PATCH 2/6] For the virtual library column stuff: avoid using refresh when invalidating libraries. --- src/calibre/gui2/search_restriction_mixin.py | 6 +++--- src/calibre/library/caches.py | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index a90d607ea9..63094b45f0 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -332,7 +332,7 @@ class SearchRestrictionMixin(object): virt_libs = db.prefs.get('virtual_libraries', {}) virt_libs[name] = search db.prefs.set('virtual_libraries', virt_libs) - db.data.invalidate_virtual_libraries_caches(db) + db.data.invalidate_virtual_libraries_caches() def do_create_edit(self, name=None): db = self.library_view.model().db @@ -342,7 +342,7 @@ class SearchRestrictionMixin(object): if name: self._remove_vl(name, reapply=False) self.add_virtual_library(db, cd.library_name, cd.library_search) - db.data.invalidate_virtual_libraries_caches(db) + db.data.invalidate_virtual_libraries_caches() if not name or name == db.data.get_base_restriction_name(): self.apply_virtual_library(cd.library_name) else: @@ -467,7 +467,7 @@ class SearchRestrictionMixin(object): return self._remove_vl(name, reapply=True) db = self.library_view.model().db - db.data.invalidate_virtual_libraries_caches(db) + db.data.invalidate_virtual_libraries_caches() self.tags_view.recount() def _remove_vl(self, name, reapply=True): diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 05d964be71..544a8e4b56 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -212,6 +212,8 @@ class CacheRow(list): # {{{ self[c] = None self._must_do = True + def refresh_virtual_libraries(self): + self._virt_libs = None # }}} class ResultCache(SearchQueryParser): # {{{ @@ -247,6 +249,8 @@ class ResultCache(SearchQueryParser): # {{{ pref_use_primary_find_in_search = prefs['use_primary_find_in_search'] self._uuid_column_index = self.FIELD_MAP['uuid'] self._uuid_map = {} + self._virt_libs_computed = False + self._ids_in_virt_libs = {} def break_cycles(self): self._data = self.field_metadata = self.FIELD_MAP = \ @@ -840,8 +844,14 @@ class ResultCache(SearchQueryParser): # {{{ current_candidates -= matches return matches - def invalidate_virtual_libraries_caches(self, db): - self.refresh(db) + def invalidate_virtual_libraries_caches(self): + self._virt_libs_computed = False + self._ids_in_virt_libs = {} + + for row in self._data: + if row is not None: + row.refresh_virtual_libraries() + row.refresh_composites() def search_raw(self, query): matches = self.parse(query) From 642b6732c7170372d75029838a0a3774122b37e8 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sat, 13 Jul 2013 11:38:23 +0200 Subject: [PATCH 3/6] [Bug 1200826] [NEW] Matching a library book to device search default --- src/calibre/gui2/dialogs/match_books.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/dialogs/match_books.py b/src/calibre/gui2/dialogs/match_books.py index 6914c8bb84..6ac7aa533c 100644 --- a/src/calibre/gui2/dialogs/match_books.py +++ b/src/calibre/gui2/dialogs/match_books.py @@ -107,6 +107,8 @@ class MatchBooks(QDialog, Ui_MatchBooks): self.buttonBox.rejected.connect(self.reject) self.ignore_next_key = False + self.search_text.setText(self.device_db[self.current_device_book_id].title) + def return_pressed(self): self.ignore_next_key = True self.do_search() From 1c75418d2065b51fd9782206705cbe938ee08c1a Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sat, 13 Jul 2013 12:52:47 +0200 Subject: [PATCH 4/6] Back the virtual library column stuff out of this branch. --- src/calibre/gui2/search_restriction_mixin.py | 7 -- src/calibre/library/caches.py | 125 ++++++------------- src/calibre/library/cli.py | 2 +- src/calibre/library/database2.py | 6 - src/calibre/library/field_metadata.py | 10 -- src/calibre/utils/formatter_functions.py | 12 +- src/calibre/utils/search_query_parser.py | 15 +-- 7 files changed, 48 insertions(+), 129 deletions(-) diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index 63094b45f0..b986a2a78e 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -332,7 +332,6 @@ class SearchRestrictionMixin(object): virt_libs = db.prefs.get('virtual_libraries', {}) virt_libs[name] = search db.prefs.set('virtual_libraries', virt_libs) - db.data.invalidate_virtual_libraries_caches() def do_create_edit(self, name=None): db = self.library_view.model().db @@ -342,11 +341,8 @@ class SearchRestrictionMixin(object): if name: self._remove_vl(name, reapply=False) self.add_virtual_library(db, cd.library_name, cd.library_search) - db.data.invalidate_virtual_libraries_caches() if not name or name == db.data.get_base_restriction_name(): self.apply_virtual_library(cd.library_name) - else: - self.tags_view.recount() def virtual_library_clicked(self): m = self.virtual_library_menu @@ -466,9 +462,6 @@ class SearchRestrictionMixin(object): default_yes=False): return self._remove_vl(name, reapply=True) - db = self.library_view.model().db - db.data.invalidate_virtual_libraries_caches() - self.tags_view.recount() def _remove_vl(self, name, reapply=True): db = self.library_view.model().db diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 544a8e4b56..e552ead591 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -2,7 +2,7 @@ # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import with_statement -__license__ = 'GPL v3' +__license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' @@ -144,8 +144,7 @@ def force_to_bool(val): class CacheRow(list): # {{{ - def __init__(self, db, composites, val, series_col, series_sort_col, - virtual_library_col): + def __init__(self, db, composites, val, series_col, series_sort_col): self.db = db self._composites = composites list.__init__(self, val) @@ -153,8 +152,6 @@ class CacheRow(list): # {{{ self._series_col = series_col self._series_sort_col = series_sort_col self._series_sort = None - self._virt_lib_col = virtual_library_col - self._virt_libs = None def __getitem__(self, col): if self._must_do: @@ -174,7 +171,7 @@ class CacheRow(list): # {{{ mi = self.db.get_metadata(id_, index_is_id=True, get_user_categories=False) for c in self._composites: - self[c] = mi.get(self._composites[c]) + self[c] = mi.get(self._composites[c]) if col == self._series_sort_col and self._series_sort is None: if self[self._series_col]: self._series_sort = title_sort(self[self._series_col]) @@ -182,26 +179,6 @@ class CacheRow(list): # {{{ else: self._series_sort = '' self[self._series_sort_col] = '' - - if col == self._virt_lib_col and self._virt_libs is None: - try: - if not getattr(self.db.data, '_virt_libs_computed', False): - self.db.data._ids_in_virt_libs = {} - for v,s in self.db.prefs.get('virtual_libraries', {}).iteritems(): - self.db.data._ids_in_virt_libs[v] = self.db.data.search_raw(s) - self.db.data._virt_libs_computed = True - r = [] - for v in self.db.prefs.get('virtual_libraries', {}).keys(): - # optimize the lookup of the ID -- it is always zero - if self[0] in self.db.data._ids_in_virt_libs[v]: - r.append(v) - from calibre.utils.icu import sort_key - self._virt_libs = ", ".join(sorted(r, key=sort_key)) - self[self._virt_lib_col] = self._virt_libs - except: - print len(self) - traceback.print_exc() - return list.__getitem__(self, col) def __getslice__(self, i, j): @@ -209,11 +186,9 @@ class CacheRow(list): # {{{ def refresh_composites(self): for c in self._composites: - self[c] = None + self[c] = None self._must_do = True - def refresh_virtual_libraries(self): - self._virt_libs = None # }}} class ResultCache(SearchQueryParser): # {{{ @@ -231,7 +206,6 @@ class ResultCache(SearchQueryParser): # {{{ self.composites[field_metadata[key]['rec_index']] = key self.series_col = field_metadata['series']['rec_index'] self.series_sort_col = field_metadata['series_sort']['rec_index'] - self.virtual_libraries_col = field_metadata['virtual_libraries']['rec_index'] self._data = [] self._map = self._map_filtered = [] self.first_sort = True @@ -249,8 +223,6 @@ class ResultCache(SearchQueryParser): # {{{ pref_use_primary_find_in_search = prefs['use_primary_find_in_search'] self._uuid_column_index = self.FIELD_MAP['uuid'] self._uuid_map = {} - self._virt_libs_computed = False - self._ids_in_virt_libs = {} def break_cycles(self): self._data = self.field_metadata = self.FIELD_MAP = \ @@ -340,12 +312,12 @@ class ResultCache(SearchQueryParser): # {{{ '<=':[2, relop_le] } - local_today = ('_today', icu_lower(_('today'))) - local_yesterday = ('_yesterday', icu_lower(_('yesterday'))) - local_thismonth = ('_thismonth', icu_lower(_('thismonth'))) - local_daysago = icu_lower(_('daysago')) - local_daysago_len = len(local_daysago) - untrans_daysago = '_daysago' + local_today = ('_today', icu_lower(_('today'))) + local_yesterday = ('_yesterday', icu_lower(_('yesterday'))) + local_thismonth = ('_thismonth', icu_lower(_('thismonth'))) + local_daysago = icu_lower(_('daysago')) + local_daysago_len = len(local_daysago) + untrans_daysago = '_daysago' untrans_daysago_len = len('_daysago') def get_dates_matches(self, location, query, candidates): @@ -441,21 +413,21 @@ class ResultCache(SearchQueryParser): # {{{ if val_func is None: loc = self.field_metadata[location]['rec_index'] - val_func = lambda item, loc = loc: item[loc] + val_func = lambda item, loc=loc: item[loc] q = '' cast = adjust = lambda x: x dt = self.field_metadata[location]['datatype'] if query == 'false': if dt == 'rating' or location == 'cover': - relop = lambda x, y: not bool(x) + relop = lambda x,y: not bool(x) else: - relop = lambda x, y: x is None + relop = lambda x,y: x is None elif query == 'true': if dt == 'rating' or location == 'cover': - relop = lambda x, y: bool(x) + relop = lambda x,y: bool(x) else: - relop = lambda x, y: x is not None + relop = lambda x,y: x is not None else: relop = None for k in self.numeric_search_relops.keys(): @@ -469,7 +441,7 @@ class ResultCache(SearchQueryParser): # {{{ cast = lambda x: int(x) elif dt == 'rating': cast = lambda x: 0 if x is None else int(x) - adjust = lambda x: x / 2 + adjust = lambda x: x/2 elif dt in ('float', 'composite'): cast = lambda x : float(x) else: # count operation @@ -477,7 +449,7 @@ class ResultCache(SearchQueryParser): # {{{ if len(query) > 1: mult = query[-1:].lower() - mult = {'k':1024., 'm': 1024.**2, 'g': 1024.**3}.get(mult, 1.0) + mult = {'k':1024.,'m': 1024.**2, 'g': 1024.**3}.get(mult, 1.0) if mult != 1.0: query = query[:-1] else: @@ -596,12 +568,12 @@ class ResultCache(SearchQueryParser): # {{{ query = icu_lower(query) return matchkind, query - local_no = icu_lower(_('no')) - local_yes = icu_lower(_('yes')) + local_no = icu_lower(_('no')) + local_yes = icu_lower(_('yes')) local_unchecked = icu_lower(_('unchecked')) - local_checked = icu_lower(_('checked')) - local_empty = icu_lower(_('empty')) - local_blank = icu_lower(_('blank')) + local_checked = icu_lower(_('checked')) + local_empty = icu_lower(_('empty')) + local_blank = icu_lower(_('blank')) local_bool_values = ( local_no, local_unchecked, '_no', 'false', local_yes, local_checked, '_yes', 'true', @@ -724,8 +696,8 @@ class ResultCache(SearchQueryParser): # {{{ if fm['is_multiple'] and \ len(query) > 1 and query.startswith('#') and \ query[1:1] in '=<>!': - vf = lambda item, loc = fm['rec_index'], \ - ms = fm['is_multiple']['cache_to_list']:\ + vf = lambda item, loc=fm['rec_index'], \ + ms=fm['is_multiple']['cache_to_list']:\ len(item[loc].split(ms)) if item[loc] is not None else 0 return self.get_numeric_matches(location, query[1:], candidates, val_func=vf) @@ -735,7 +707,7 @@ class ResultCache(SearchQueryParser): # {{{ if fm.get('is_csp', False): if location == 'identifiers' and original_location == 'isbn': return self.get_keypair_matches('identifiers', - '=isbn:' + query, candidates) + '=isbn:'+query, candidates) return self.get_keypair_matches(location, query, candidates) # check for user categories @@ -787,7 +759,7 @@ class ResultCache(SearchQueryParser): # {{{ q = canonicalize_lang(query) if q is None: lm = lang_map() - rm = {v.lower():k for k, v in lm.iteritems()} + rm = {v.lower():k for k,v in lm.iteritems()} q = rm.get(query, query) else: q = query @@ -800,7 +772,7 @@ class ResultCache(SearchQueryParser): # {{{ if not item[loc]: if q == 'false' and matchkind == CONTAINS_MATCH: matches.add(item[0]) - continue # item is empty. No possible matches below + continue # item is empty. No possible matches below if q == 'false'and matchkind == CONTAINS_MATCH: # Field has something in it, so a false query does not match continue @@ -844,19 +816,6 @@ class ResultCache(SearchQueryParser): # {{{ current_candidates -= matches return matches - def invalidate_virtual_libraries_caches(self): - self._virt_libs_computed = False - self._ids_in_virt_libs = {} - - for row in self._data: - if row is not None: - row.refresh_virtual_libraries() - row.refresh_composites() - - def search_raw(self, query): - matches = self.parse(query) - return matches - def search(self, query, return_matches=False): ans = self.search_getting_ids(query, self.search_restriction, set_restriction_count=True) @@ -1014,11 +973,10 @@ class ResultCache(SearchQueryParser): # {{{ try: self._data[id] = CacheRow(db, self.composites, db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0], - self.series_col, self.series_sort_col, - self.virtual_libraries_col) + self.series_col, self.series_sort_col) self._data[id].append(db.book_on_device_string(id)) - self._data[id].extend((self.marked_ids_dict.get(id, None), None, None)) - self._virt_libs_computed = False + self._data[id].append(self.marked_ids_dict.get(id, None)) + self._data[id].append(None) self._uuid_map[self._data[id][self._uuid_column_index]] = id except IndexError: return None @@ -1031,15 +989,14 @@ class ResultCache(SearchQueryParser): # {{{ def books_added(self, ids, db): if not ids: return - self._data.extend(repeat(None, max(ids) - len(self._data) + 2)) + self._data.extend(repeat(None, max(ids)-len(self._data)+2)) for id in ids: self._data[id] = CacheRow(db, self.composites, db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0], - self.series_col, self.series_sort_col, - self.virtual_libraries_col) + self.series_col, self.series_sort_col) self._data[id].append(db.book_on_device_string(id)) - self._data[id].extend((self.marked_ids_dict.get(id, None), None, None)) - self._virt_libs_computed = False + self._data[id].append(self.marked_ids_dict.get(id, None)) + self._data[id].append(None) # Series sort column self._uuid_map[self._data[id][self._uuid_column_index]] = id self._map[0:0] = ids self._map_filtered[0:0] = ids @@ -1063,22 +1020,20 @@ class ResultCache(SearchQueryParser): # {{{ db.initialize_template_cache() temp = db.conn.get('SELECT * FROM meta2') - self._data = list(itertools.repeat(None, temp[-1][0] + 2)) if temp else [] + self._data = list(itertools.repeat(None, temp[-1][0]+2)) if temp else [] for r in temp: self._data[r[0]] = CacheRow(db, self.composites, r, - self.series_col, self.series_sort_col, - self.virtual_libraries_col) + self.series_col, self.series_sort_col) self._uuid_map[self._data[r[0]][self._uuid_column_index]] = r[0] for item in self._data: if item is not None: item.append(db.book_on_device_string(item[0])) - # Temp mark, series_sort, virtual_library columns - item.extend((None, None, None)) + # Temp mark and series_sort columns + item.extend((None, None)) - self._virt_libs_computed = False marked_col = self.FIELD_MAP['marked'] - for id_, val in self.marked_ids_dict.iteritems(): + for id_,val in self.marked_ids_dict.iteritems(): try: self._data[id_][marked_col] = val except: @@ -1179,7 +1134,7 @@ class SortKeyGenerator(object): for i, candidate in enumerate( ('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')): if val.endswith(candidate): - p = 1024 ** (i) + p = 1024**(i) val = val[:-len(candidate)].strip() break val = locale.atof(val) * p diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index a86ec7ef6d..547cc5bc08 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -575,7 +575,7 @@ def command_set_metadata(args, dbpath): for key in sorted(db.field_metadata.all_field_keys()): m = db.field_metadata[key] if (key not in {'formats', 'series_sort', 'ondevice', 'path', - 'virtual_libraries', 'last_modified'} and m['is_editable'] and m['name']): + 'last_modified'} and m['is_editable'] and m['name']): yield key, m if m['datatype'] == 'series': si = m.copy() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 00c6fb057f..61c1653cee 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -449,8 +449,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.field_metadata.set_field_record_index('marked', base, prefer_custom=False) self.FIELD_MAP['series_sort'] = base = base+1 self.field_metadata.set_field_record_index('series_sort', base, prefer_custom=False) - self.FIELD_MAP['virtual_libraries'] = base = base+1 - self.field_metadata.set_field_record_index('virtual_libraries', base, prefer_custom=False) script = ''' DROP VIEW IF EXISTS meta2; @@ -994,10 +992,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): mi.book_size = row[fm['size']] mi.ondevice_col= row[fm['ondevice']] mi.last_modified = row[fm['last_modified']] - - mi._base_db_row = row # So the formatter functions can see the underlying data - mi._virt_lib_column = fm['virtual_libraries'] - formats = row[fm['formats']] mi.format_metadata = {} if not formats: diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index a96f819b58..08c26e95a9 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -387,16 +387,6 @@ class FieldMetadata(dict): 'is_custom':False, 'is_category':False, 'is_csp': False}), - ('virtual_libraries', {'table':None, - 'column':None, - 'datatype':'text', - 'is_multiple':{}, - 'kind':'field', - 'name':_('Virtual Libraries'), - 'search_terms':['virtual_libraries'], - 'is_custom':False, - 'is_category':False, - 'is_csp': False}), ] # }}} diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index c94467fec0..73dad7422b 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -1209,19 +1209,9 @@ class BuiltinFinishFormatting(BuiltinFormatterFunction): return val return prefix + formatter._do_format(val, fmt) + suffix -class BuiltinBookInVirtualLibraries(BuiltinFormatterFunction): - name = 'book_in_virtual_libraries' - arg_count = 0 - category = 'Get values from metadata' - __doc__ = doc = _('book_in_virtual_libraries() -- returns a list of ' - 'virtual libraries that this book is in.') - - def evaluate(self, formatter, kwargs, mi, locals_): - return mi._base_db_row[mi._virt_lib_column ] - _formatter_builtins = [ BuiltinAdd(), BuiltinAnd(), BuiltinApproximateFormats(), - BuiltinAssign(), BuiltinBookInVirtualLibraries(), BuiltinBooksize(), + BuiltinAssign(), BuiltinBooksize(), BuiltinCapitalize(), BuiltinCmp(), BuiltinContains(), BuiltinCount(), BuiltinCurrentLibraryName(), BuiltinCurrentLibraryPath(), BuiltinDaysBetween(), BuiltinDivide(), BuiltinEval(), BuiltinFirstNonEmpty(), diff --git a/src/calibre/utils/search_query_parser.py b/src/calibre/utils/search_query_parser.py index 08a70a533d..2682088681 100644 --- a/src/calibre/utils/search_query_parser.py +++ b/src/calibre/utils/search_query_parser.py @@ -294,7 +294,6 @@ class SearchQueryParser(object): def __init__(self, locations, test=False, optimize=False): self.sqp_initialize(locations, test=test, optimize=optimize) - self.sqp_parsed_search_cache = {} self.parser = Parser() def sqp_change_locations(self, locations): @@ -309,7 +308,8 @@ class SearchQueryParser(object): # empty the list of searches used for recursion testing self.recurse_level = 0 self.searches_seen = set([]) - return self._parse(query) + candidates = self.universal_set() + return self._parse(query, candidates) # this parse is used internally because it doesn't clear the # recursive search test list. However, we permit seeing the @@ -317,13 +317,10 @@ class SearchQueryParser(object): # another search. def _parse(self, query, candidates=None): self.recurse_level += 1 - res = self.sqp_parsed_search_cache.get(query, None) - if res is None: - try: - res = self.parser.parse(query, self.locations) - self.sqp_parsed_search_cache[query] = res - except RuntimeError: - raise ParseException(_('Failed to parse query, recursion limit reached: %s')%repr(query)) + try: + res = self.parser.parse(query, self.locations) + except RuntimeError: + raise ParseException(_('Failed to parse query, recursion limit reached: %s')%repr(query)) if candidates is None: candidates = self.universal_set() t = self.evaluate(res, candidates) From e336d8a2b980d78c8b0e2d943b57dd10f9f77993 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Fri, 19 Jul 2013 14:00:35 +0200 Subject: [PATCH 5/6] Strip spaces off of function names before looking them up. They shouldn't ever be significant. --- src/calibre/utils/formatter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index eff38203a0..2fa5901dde 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -137,6 +137,7 @@ class _Parser(object): # We have a function. # Check if it is a known one. We do this here so error reporting is # better, as it can identify the tokens near the problem. + id = id.strip() if id not in funcs: self.error(_('unknown function {0}').format(id)) @@ -246,6 +247,7 @@ class _CompileParser(_Parser): # We have a function. # Check if it is a known one. We do this here so error reporting is # better, as it can identify the tokens near the problem. + id = id.strip() if id not in funcs: self.error(_('unknown function {0}').format(id)) @@ -457,7 +459,7 @@ class TemplateFormatter(string.Formatter): colon += 1 funcs = formatter_functions().get_functions() - fname = fmt[colon:p] + fname = fmt[colon:p].strip() if fname in funcs: func = funcs[fname] if func.arg_count == 2: From 29e2886267825443616a2310bb5b944c0a72cd6a Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Fri, 19 Jul 2013 14:01:47 +0200 Subject: [PATCH 6/6] Implement enforcement of unique custom template function names. --- src/calibre/ebooks/metadata/worker.py | 2 +- .../gui2/preferences/template_functions.py | 2 +- src/calibre/gui2/ui.py | 3 + src/calibre/library/database2.py | 3 +- src/calibre/utils/formatter_functions.py | 56 +++++++++++++++++-- 5 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/calibre/ebooks/metadata/worker.py b/src/calibre/ebooks/metadata/worker.py index 660240571b..e8147958d8 100644 --- a/src/calibre/ebooks/metadata/worker.py +++ b/src/calibre/ebooks/metadata/worker.py @@ -318,7 +318,7 @@ def save_book(ids, dpath, plugboards, template_functions, path, recs, from calibre.library.save_to_disk import config, save_serialized_to_disk from calibre.customize.ui import apply_null_metadata from calibre.utils.formatter_functions import load_user_template_functions - load_user_template_functions(template_functions) + load_user_template_functions('', template_functions) opts = config().parse() for name in recs: setattr(opts, name, recs[name]) diff --git a/src/calibre/gui2/preferences/template_functions.py b/src/calibre/gui2/preferences/template_functions.py index f5203e9cdd..4cc07fee3a 100644 --- a/src/calibre/gui2/preferences/template_functions.py +++ b/src/calibre/gui2/preferences/template_functions.py @@ -223,7 +223,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): if f in self.builtins: continue func = self.funcs[f] - formatter_functions().register_function(func) + formatter_functions().register_function(self.db.library_id, func) pref_value.append((func.name, func.doc, func.arg_count, func.program_text)) self.db.prefs.set('user_template_functions', pref_value) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index e54d06e671..05f36c2709 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -580,6 +580,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ olddb = self.library_view.model().db if copy_structure: default_prefs = olddb.prefs + + from calibre.utils.formatter_functions import unload_user_template_functions + unload_user_template_functions(olddb.library_id ) except: olddb = None try: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 8ea7e75b59..7942c69574 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -325,7 +325,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.prefs.set('user_categories', user_cats) if not self.is_second_db: - load_user_template_functions(self.prefs.get('user_template_functions', [])) + load_user_template_functions(self.library_id, + self.prefs.get('user_template_functions', [])) # Load the format filename cache self.refresh_format_cache() diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 73dad7422b..f8d62c367a 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -10,6 +10,7 @@ __docformat__ = 'restructuredtext en' import inspect, re, traceback from math import trunc +from collections import defaultdict from calibre import human_readable from calibre.constants import DEBUG @@ -25,6 +26,7 @@ class FormatterFunctions(object): def __init__(self): self._builtins = {} self._functions = {} + self._functions_from_library = defaultdict(list) def register_builtin(self, func_class): if not isinstance(func_class, FormatterFunction): @@ -38,14 +40,24 @@ class FormatterFunctions(object): for a in func_class.aliases: self._functions[a] = func_class - def register_function(self, func_class): + def register_function(self, library_uuid, func_class, replace=False): if not isinstance(func_class, FormatterFunction): raise ValueError('Class %s is not an instance of FormatterFunction'%( func_class.__class__.__name__)) name = func_class.name - if name in self._functions: + if not replace and name in self._functions: raise ValueError('Name %s already used'%name) self._functions[name] = func_class + self._functions_from_library[library_uuid].append(name) + + def function_exists(self, name): + return self._functions.get(name, None) + + def unregister_functions(self, library_uuid): + if library_uuid in self._functions_from_library: + for name in self._functions_from_library[library_uuid]: + self._functions.pop(name) + self._functions_from_library.pop(library_uuid) def get_builtins(self): return self._builtins @@ -1259,11 +1271,45 @@ class UserFunction(FormatterUserFunction): cls = locals_['UserFunction'](name, doc, arg_count, eval_func) return cls -def load_user_template_functions(funcs): - formatter_functions().reset_to_builtins() +error_function_body = ('def evaluate(self, formatter, kwargs, mi, locals):\n' + '\treturn "' + + _('Duplicate user function name {0}. ' + 'Change the name or ensure that the functions are identical') + + '"') + +def load_user_template_functions(library_uuid, funcs): + unload_user_template_functions(library_uuid) + for func in funcs: try: + # Force a name conflict to test the logic + # if func[0] == 'myFunc2': + # func[0] = 'myFunc3' + + # Compile the function so that the tab processing is done on the + # source. This helps ensure that if the function already is defined + # then white space differences don't cause them to compare differently + cls = compile_user_function(*func) - formatter_functions().register_function(cls) + f = formatter_functions().function_exists(cls.name) + replace = False + if f is not None: + existing_body = f.program_text + new_body = cls.program_text + if new_body != existing_body: + # Change the body of the template function to one that will + # return an error message. Also change the arg count to + # -1 (variable) to avoid template compilation errors + replace = True + func[3] = error_function_body.format(func[0]) + func[2] = -1 + cls = compile_user_function(*func) + else: + continue + + formatter_functions().register_function(library_uuid, cls, replace=replace) except: traceback.print_exc() + +def unload_user_template_functions(library_uuid): + formatter_functions().unregister_functions(library_uuid) \ No newline at end of file