diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index cb12905772..df6cb66958 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -140,9 +140,11 @@ class Cache: EventType = EventType fts_indexing_sleep_time = 4 # seconds - def __init__(self, backend): + def __init__(self, backend, library_database_instance=None): self.shutting_down = False self.backend = backend + self.library_database_instance = (None if library_database_instance is None else + weakref.ref(library_database_instance)) self.event_dispatcher = EventDispatcher() self.fields = {} self.composites = {} diff --git a/src/calibre/db/legacy.py b/src/calibre/db/legacy.py index 3f53f4b416..a17e15ab85 100644 --- a/src/calibre/db/legacy.py +++ b/src/calibre/db/legacy.py @@ -188,7 +188,7 @@ class LibraryDatabase: read_only=read_only, restore_all_prefs=restore_all_prefs, progress_callback=progress_callback, load_user_formatter_functions=not is_second_db) - cache = self.new_api = Cache(backend) + cache = self.new_api = Cache(backend, library_database_instance=self) cache.init() self.data = View(cache) self.id = self.data.index_to_id diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 368e97460d..e10c4c5481 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -15,6 +15,7 @@ from math import modf from calibre import prints from calibre.constants import DEBUG from calibre.ebooks.metadata.book.base import field_metadata +from calibre.utils.config import tweaks from calibre.utils.formatter_functions import formatter_functions from calibre.utils.icu import strcmp from polyglot.builtins import error_message @@ -1697,8 +1698,9 @@ class TemplateFormatter(string.Formatter): except StopException as e: ans = error_message(e) except Exception as e: - if DEBUG: # and getattr(e, 'is_locking_error', False): - traceback.print_exc() + if DEBUG: + if tweaks.get('show_stack_traces_in_formatter', True): + traceback.print_exc() if column_name: prints('Error evaluating column named:', column_name) ans = error_value + ' ' + error_message(e) diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index e19c94f1ef..844a2ce12c 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -12,10 +12,10 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import inspect, re, traceback, numbers +from contextlib import contextmanager, suppress from datetime import datetime, timedelta from functools import partial from math import trunc, floor, ceil, modf -from contextlib import suppress from calibre import human_readable, prints, prepare_string_for_xml from calibre.constants import DEBUG @@ -77,6 +77,8 @@ class FormatterFunctions: # 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 + if DEBUG: + print(f'attempt to replace formatter function {f.name} with a different body') replace = True func = [cls.name, '', -1, self.error_function_body.format(cls.name)] cls = compile_user_function(*func) @@ -142,6 +144,28 @@ class FormatterFunction: if isinstance(ret, (numbers.Number, bool)): return str(ret) + def only_in_gui_error(self): + raise ValueError(_('The function {} can be used only in the GUI').format(self.name)) + + @contextmanager + def get_database(self, mi): + proxy = mi.get('_proxy_metadata', None) + if proxy is None: + self.only_in_gui_error() + wr = proxy.get('_db', None) + if wr is None: + raise ValueError(_('In function {}: The database has been closed').format(self.name)) + cache = wr() + if cache is None: + raise ValueError(_('In function {}: The database has been closed').format(self.name)) + wr = getattr(cache, 'library_database_instance', None) + if wr is None: + self.only_in_gui_error() + db = wr() + if db is None: + raise ValueError(_('In function {}: The database has been closed').format(self.name)) + yield db + class BuiltinFormatterFunction(FormatterFunction): @@ -928,7 +952,7 @@ class BuiltinApproximateFormats(BuiltinFormatterFunction): return '' data = sorted(fmt_data) return ','.join(v.upper() for v in data) - return _('This function can be used only in the GUI') + self.only_in_gui_error() class BuiltinFormatsModtimes(BuiltinFormatterFunction): @@ -1236,7 +1260,7 @@ class BuiltinBooksize(BuiltinFormatterFunction): except: pass return '' - return _('This function can be used only in the GUI') + self.only_in_gui_error() class BuiltinOndevice(BuiltinFormatterFunction): @@ -1255,7 +1279,7 @@ class BuiltinOndevice(BuiltinFormatterFunction): if mi._proxy_metadata.ondevice_col: return _('Yes') return '' - return _('This function can be used only in the GUI') + self.only_in_gui_error() class BuiltinAnnotationCount(BuiltinFormatterFunction): @@ -1267,11 +1291,9 @@ class BuiltinAnnotationCount(BuiltinFormatterFunction): 'This function works only in the GUI.') def evaluate(self, formatter, kwargs, mi, locals): - with suppress(Exception): - from calibre.gui2.ui import get_gui - c = get_gui().current_db.new_api.annotation_count_for_book(mi.id) + with self.get_database(mi) as db: + c = db.new_api.annotation_count_for_book(mi.id) return '' if c == 0 else str(c) - return _('This function can be used only in the GUI') class BuiltinIsMarked(BuiltinFormatterFunction): @@ -1284,11 +1306,9 @@ class BuiltinIsMarked(BuiltinFormatterFunction): "marks. Returns '' if the book is not marked.") def evaluate(self, formatter, kwargs, mi, locals): - with suppress(Exception): - from calibre.gui2.ui import get_gui - c = get_gui().current_db.data.get_marked(mi.id) + with self.get_database(mi) as db: + c = db.data.get_marked(mi.id) return c if c else '' - return _('This function can be used only in the GUI') class BuiltinSeriesSort(BuiltinFormatterFunction): @@ -1850,14 +1870,12 @@ class BuiltinVirtualLibraries(BuiltinFormatterFunction): 'column\'s value in your save/send templates') def evaluate(self, formatter, kwargs, mi, locals_): - with suppress(Exception): + with self.get_database(mi) as db: try: - from calibre.gui2.ui import get_gui - a = get_gui().current_db.data.get_virtual_libraries_for_books((mi.id,)) + a = db.data.get_virtual_libraries_for_books((mi.id,)) return ', '.join(a[mi.id]) except ValueError as v: return str(v) - return _('This function can be used only in the GUI') class BuiltinCurrentVirtualLibraryName(BuiltinFormatterFunction): @@ -1870,10 +1888,8 @@ class BuiltinCurrentVirtualLibraryName(BuiltinFormatterFunction): 'Example: "program: current_virtual_library_name()".') def evaluate(self, formatter, kwargs, mi, locals): - with suppress(Exception): - from calibre.gui2.ui import get_gui - return get_gui().current_db.data.get_base_restriction_name() - return _('This function can be used only in the GUI') + with self.get_database(mi) as db: + return db.data.get_base_restriction_name() class BuiltinUserCategories(BuiltinFormatterFunction): @@ -1893,7 +1909,7 @@ class BuiltinUserCategories(BuiltinFormatterFunction): cats = {k for k, v in iteritems(mi._proxy_metadata.user_categories) if v} cats = sorted(cats, key=sort_key) return ', '.join(cats) - return _('This function can be used only in the GUI') + self.only_in_gui_error() class BuiltinTransliterate(BuiltinFormatterFunction): @@ -1934,7 +1950,7 @@ class BuiltinAuthorLinks(BuiltinFormatterFunction): return '' names = sorted(link_data.keys(), key=sort_key) return pair_sep.join(n + val_sep + link_data[n] for n in names) - return _('This function can be used only in the GUI') + self.only_in_gui_error() class BuiltinAuthorSorts(BuiltinFormatterFunction): @@ -1970,6 +1986,8 @@ class BuiltinConnectedDeviceName(BuiltinFormatterFunction): "'carda' and 'cardb'. This function works only in the GUI.") def evaluate(self, formatter, kwargs, mi, locals, storage_location): + # We can't use get_database() here because we need the device manager. + # In other words, the function really does need the GUI with suppress(Exception): # Do the import here so that we don't entangle the GUI when using # command line functions @@ -1989,7 +2007,7 @@ class BuiltinConnectedDeviceName(BuiltinFormatterFunction): except Exception: traceback.print_exc() raise - return _('This function can be used only in the GUI') + self.only_in_gui_error() class BuiltinConnectedDeviceUUID(BuiltinFormatterFunction): @@ -2004,6 +2022,8 @@ class BuiltinConnectedDeviceUUID(BuiltinFormatterFunction): "the GUI.") def evaluate(self, formatter, kwargs, mi, locals, storage_location): + # We can't use get_database() here because we need the device manager. + # In other words, the function really does need the GUI with suppress(Exception): # Do the import here so that we don't entangle the GUI when using # command line functions @@ -2023,7 +2043,7 @@ class BuiltinConnectedDeviceUUID(BuiltinFormatterFunction): except Exception: traceback.print_exc() raise - return _('This function can be used only in the GUI') + self.only_in_gui_error() class BuiltinCheckYesNo(BuiltinFormatterFunction): @@ -2044,6 +2064,10 @@ class BuiltinCheckYesNo(BuiltinFormatterFunction): 'is usually used by the test() or is_empty() functions.') def evaluate(self, formatter, kwargs, mi, locals, field, is_undefined, is_false, is_true): + # 'field' is a lookup name, not a value + with self.get_database(mi) as db: + if field not in db.field_metadata: + raise ValueError(_("The column {} doesn't exist").format(field)) res = getattr(mi, field, None) if res is None: if is_undefined == '1': @@ -2239,15 +2263,13 @@ class BuiltinBookCount(BuiltinFormatterFunction): if (not tweaks.get('allow_template_database_functions_in_composites', False) and formatter.global_vars.get(rendering_composite_name, None)): raise ValueError(_('The book_count() function cannot be used in a composite column')) - with suppress(Exception): + with self.get_database(mi) as db: try: - from calibre.gui2.ui import get_gui - ids = get_gui().current_db.search_getting_ids(query, None, - use_virtual_library=use_vl != '0') + ids = db.search_getting_ids(query, None, use_virtual_library=use_vl != '0') return len(ids) except Exception: traceback.print_exc() - return _('This function can be used only in the GUI') + self.only_in_gui_error() class BuiltinBookValues(BuiltinFormatterFunction): @@ -2265,25 +2287,21 @@ class BuiltinBookValues(BuiltinFormatterFunction): if (not tweaks.get('allow_template_database_functions_in_composites', False) and formatter.global_vars.get(rendering_composite_name, None)): raise ValueError(_('The book_values() function cannot be used in a composite column')) - try: - from calibre.gui2.ui import get_gui - db = get_gui().current_db - except: - return _('This function can be used only in the GUI') - if column not in db.field_metadata: - raise ValueError(_("The column {} doesn't exist").format(column)) - try: - ids = db.search_getting_ids(query, None, use_virtual_library=use_vl != '0') - s = set() - for id_ in ids: - f = db.new_api.get_proxy_metadata(id_).get(column, None) - if isinstance(f, (tuple, list)): - s.update(f) - elif f: - s.add(str(f)) - return sep.join(s) - except Exception as e: - raise ValueError(e) + with self.get_database(mi) as db: + if column not in db.field_metadata: + raise ValueError(_("The column {} doesn't exist").format(column)) + try: + ids = db.search_getting_ids(query, None, use_virtual_library=use_vl != '0') + s = set() + for id_ in ids: + f = db.new_api.get_proxy_metadata(id_).get(column, None) + if isinstance(f, (tuple, list)): + s.update(f) + elif f: + s.add(str(f)) + return sep.join(s) + except Exception as e: + raise ValueError(e) _formatter_builtins = [