The formatter functions database code we have been discussing

This commit is contained in:
Charles Haley 2022-07-12 14:43:27 +01:00
parent 288a22d438
commit f0677655f8
4 changed files with 74 additions and 52 deletions

View File

@ -140,9 +140,11 @@ class Cache:
EventType = EventType EventType = EventType
fts_indexing_sleep_time = 4 # seconds fts_indexing_sleep_time = 4 # seconds
def __init__(self, backend): def __init__(self, backend, library_database_instance=None):
self.shutting_down = False self.shutting_down = False
self.backend = backend 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.event_dispatcher = EventDispatcher()
self.fields = {} self.fields = {}
self.composites = {} self.composites = {}

View File

@ -188,7 +188,7 @@ class LibraryDatabase:
read_only=read_only, restore_all_prefs=restore_all_prefs, read_only=read_only, restore_all_prefs=restore_all_prefs,
progress_callback=progress_callback, progress_callback=progress_callback,
load_user_formatter_functions=not is_second_db) 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() cache.init()
self.data = View(cache) self.data = View(cache)
self.id = self.data.index_to_id self.id = self.data.index_to_id

View File

@ -15,6 +15,7 @@ from math import modf
from calibre import prints from calibre import prints
from calibre.constants import DEBUG from calibre.constants import DEBUG
from calibre.ebooks.metadata.book.base import field_metadata 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.formatter_functions import formatter_functions
from calibre.utils.icu import strcmp from calibre.utils.icu import strcmp
from polyglot.builtins import error_message from polyglot.builtins import error_message
@ -1697,8 +1698,9 @@ class TemplateFormatter(string.Formatter):
except StopException as e: except StopException as e:
ans = error_message(e) ans = error_message(e)
except Exception as e: except Exception as e:
if DEBUG: # and getattr(e, 'is_locking_error', False): if DEBUG:
traceback.print_exc() if tweaks.get('show_stack_traces_in_formatter', True):
traceback.print_exc()
if column_name: if column_name:
prints('Error evaluating column named:', column_name) prints('Error evaluating column named:', column_name)
ans = error_value + ' ' + error_message(e) ans = error_value + ' ' + error_message(e)

View File

@ -12,10 +12,10 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import inspect, re, traceback, numbers import inspect, re, traceback, numbers
from contextlib import contextmanager, suppress
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import partial from functools import partial
from math import trunc, floor, ceil, modf from math import trunc, floor, ceil, modf
from contextlib import suppress
from calibre import human_readable, prints, prepare_string_for_xml from calibre import human_readable, prints, prepare_string_for_xml
from calibre.constants import DEBUG from calibre.constants import DEBUG
@ -77,6 +77,8 @@ class FormatterFunctions:
# Change the body of the template function to one that will # Change the body of the template function to one that will
# return an error message. Also change the arg count to # return an error message. Also change the arg count to
# -1 (variable) to avoid template compilation errors # -1 (variable) to avoid template compilation errors
if DEBUG:
print(f'attempt to replace formatter function {f.name} with a different body')
replace = True replace = True
func = [cls.name, '', -1, self.error_function_body.format(cls.name)] func = [cls.name, '', -1, self.error_function_body.format(cls.name)]
cls = compile_user_function(*func) cls = compile_user_function(*func)
@ -142,6 +144,28 @@ class FormatterFunction:
if isinstance(ret, (numbers.Number, bool)): if isinstance(ret, (numbers.Number, bool)):
return str(ret) 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): class BuiltinFormatterFunction(FormatterFunction):
@ -928,7 +952,7 @@ class BuiltinApproximateFormats(BuiltinFormatterFunction):
return '' return ''
data = sorted(fmt_data) data = sorted(fmt_data)
return ','.join(v.upper() for v in 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): class BuiltinFormatsModtimes(BuiltinFormatterFunction):
@ -1236,7 +1260,7 @@ class BuiltinBooksize(BuiltinFormatterFunction):
except: except:
pass pass
return '' return ''
return _('This function can be used only in the GUI') self.only_in_gui_error()
class BuiltinOndevice(BuiltinFormatterFunction): class BuiltinOndevice(BuiltinFormatterFunction):
@ -1255,7 +1279,7 @@ class BuiltinOndevice(BuiltinFormatterFunction):
if mi._proxy_metadata.ondevice_col: if mi._proxy_metadata.ondevice_col:
return _('Yes') return _('Yes')
return '' return ''
return _('This function can be used only in the GUI') self.only_in_gui_error()
class BuiltinAnnotationCount(BuiltinFormatterFunction): class BuiltinAnnotationCount(BuiltinFormatterFunction):
@ -1267,11 +1291,9 @@ class BuiltinAnnotationCount(BuiltinFormatterFunction):
'This function works only in the GUI.') 'This function works only in the GUI.')
def evaluate(self, formatter, kwargs, mi, locals): def evaluate(self, formatter, kwargs, mi, locals):
with suppress(Exception): with self.get_database(mi) as db:
from calibre.gui2.ui import get_gui c = db.new_api.annotation_count_for_book(mi.id)
c = get_gui().current_db.new_api.annotation_count_for_book(mi.id)
return '' if c == 0 else str(c) return '' if c == 0 else str(c)
return _('This function can be used only in the GUI')
class BuiltinIsMarked(BuiltinFormatterFunction): class BuiltinIsMarked(BuiltinFormatterFunction):
@ -1284,11 +1306,9 @@ class BuiltinIsMarked(BuiltinFormatterFunction):
"marks. Returns '' if the book is not marked.") "marks. Returns '' if the book is not marked.")
def evaluate(self, formatter, kwargs, mi, locals): def evaluate(self, formatter, kwargs, mi, locals):
with suppress(Exception): with self.get_database(mi) as db:
from calibre.gui2.ui import get_gui c = db.data.get_marked(mi.id)
c = get_gui().current_db.data.get_marked(mi.id)
return c if c else '' return c if c else ''
return _('This function can be used only in the GUI')
class BuiltinSeriesSort(BuiltinFormatterFunction): class BuiltinSeriesSort(BuiltinFormatterFunction):
@ -1850,14 +1870,12 @@ class BuiltinVirtualLibraries(BuiltinFormatterFunction):
'column\'s value in your save/send templates') 'column\'s value in your save/send templates')
def evaluate(self, formatter, kwargs, mi, locals_): def evaluate(self, formatter, kwargs, mi, locals_):
with suppress(Exception): with self.get_database(mi) as db:
try: try:
from calibre.gui2.ui import get_gui a = db.data.get_virtual_libraries_for_books((mi.id,))
a = get_gui().current_db.data.get_virtual_libraries_for_books((mi.id,))
return ', '.join(a[mi.id]) return ', '.join(a[mi.id])
except ValueError as v: except ValueError as v:
return str(v) return str(v)
return _('This function can be used only in the GUI')
class BuiltinCurrentVirtualLibraryName(BuiltinFormatterFunction): class BuiltinCurrentVirtualLibraryName(BuiltinFormatterFunction):
@ -1870,10 +1888,8 @@ class BuiltinCurrentVirtualLibraryName(BuiltinFormatterFunction):
'Example: "program: current_virtual_library_name()".') 'Example: "program: current_virtual_library_name()".')
def evaluate(self, formatter, kwargs, mi, locals): def evaluate(self, formatter, kwargs, mi, locals):
with suppress(Exception): with self.get_database(mi) as db:
from calibre.gui2.ui import get_gui return db.data.get_base_restriction_name()
return get_gui().current_db.data.get_base_restriction_name()
return _('This function can be used only in the GUI')
class BuiltinUserCategories(BuiltinFormatterFunction): 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 = {k for k, v in iteritems(mi._proxy_metadata.user_categories) if v}
cats = sorted(cats, key=sort_key) cats = sorted(cats, key=sort_key)
return ', '.join(cats) return ', '.join(cats)
return _('This function can be used only in the GUI') self.only_in_gui_error()
class BuiltinTransliterate(BuiltinFormatterFunction): class BuiltinTransliterate(BuiltinFormatterFunction):
@ -1934,7 +1950,7 @@ class BuiltinAuthorLinks(BuiltinFormatterFunction):
return '' return ''
names = sorted(link_data.keys(), key=sort_key) names = sorted(link_data.keys(), key=sort_key)
return pair_sep.join(n + val_sep + link_data[n] for n in names) 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): class BuiltinAuthorSorts(BuiltinFormatterFunction):
@ -1970,6 +1986,8 @@ class BuiltinConnectedDeviceName(BuiltinFormatterFunction):
"'carda' and 'cardb'. This function works only in the GUI.") "'carda' and 'cardb'. This function works only in the GUI.")
def evaluate(self, formatter, kwargs, mi, locals, storage_location): 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): with suppress(Exception):
# Do the import here so that we don't entangle the GUI when using # Do the import here so that we don't entangle the GUI when using
# command line functions # command line functions
@ -1989,7 +2007,7 @@ class BuiltinConnectedDeviceName(BuiltinFormatterFunction):
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
raise raise
return _('This function can be used only in the GUI') self.only_in_gui_error()
class BuiltinConnectedDeviceUUID(BuiltinFormatterFunction): class BuiltinConnectedDeviceUUID(BuiltinFormatterFunction):
@ -2004,6 +2022,8 @@ class BuiltinConnectedDeviceUUID(BuiltinFormatterFunction):
"the GUI.") "the GUI.")
def evaluate(self, formatter, kwargs, mi, locals, storage_location): 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): with suppress(Exception):
# Do the import here so that we don't entangle the GUI when using # Do the import here so that we don't entangle the GUI when using
# command line functions # command line functions
@ -2023,7 +2043,7 @@ class BuiltinConnectedDeviceUUID(BuiltinFormatterFunction):
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
raise raise
return _('This function can be used only in the GUI') self.only_in_gui_error()
class BuiltinCheckYesNo(BuiltinFormatterFunction): class BuiltinCheckYesNo(BuiltinFormatterFunction):
@ -2044,6 +2064,10 @@ class BuiltinCheckYesNo(BuiltinFormatterFunction):
'is usually used by the test() or is_empty() functions.') 'is usually used by the test() or is_empty() functions.')
def evaluate(self, formatter, kwargs, mi, locals, field, is_undefined, is_false, is_true): 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) res = getattr(mi, field, None)
if res is None: if res is None:
if is_undefined == '1': if is_undefined == '1':
@ -2239,15 +2263,13 @@ class BuiltinBookCount(BuiltinFormatterFunction):
if (not tweaks.get('allow_template_database_functions_in_composites', False) and if (not tweaks.get('allow_template_database_functions_in_composites', False) and
formatter.global_vars.get(rendering_composite_name, None)): formatter.global_vars.get(rendering_composite_name, None)):
raise ValueError(_('The book_count() function cannot be used in a composite column')) raise ValueError(_('The book_count() function cannot be used in a composite column'))
with suppress(Exception): with self.get_database(mi) as db:
try: try:
from calibre.gui2.ui import get_gui ids = db.search_getting_ids(query, None, use_virtual_library=use_vl != '0')
ids = get_gui().current_db.search_getting_ids(query, None,
use_virtual_library=use_vl != '0')
return len(ids) return len(ids)
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
return _('This function can be used only in the GUI') self.only_in_gui_error()
class BuiltinBookValues(BuiltinFormatterFunction): class BuiltinBookValues(BuiltinFormatterFunction):
@ -2265,25 +2287,21 @@ class BuiltinBookValues(BuiltinFormatterFunction):
if (not tweaks.get('allow_template_database_functions_in_composites', False) and if (not tweaks.get('allow_template_database_functions_in_composites', False) and
formatter.global_vars.get(rendering_composite_name, None)): formatter.global_vars.get(rendering_composite_name, None)):
raise ValueError(_('The book_values() function cannot be used in a composite column')) raise ValueError(_('The book_values() function cannot be used in a composite column'))
try: with self.get_database(mi) as db:
from calibre.gui2.ui import get_gui if column not in db.field_metadata:
db = get_gui().current_db raise ValueError(_("The column {} doesn't exist").format(column))
except: try:
return _('This function can be used only in the GUI') ids = db.search_getting_ids(query, None, use_virtual_library=use_vl != '0')
if column not in db.field_metadata: s = set()
raise ValueError(_("The column {} doesn't exist").format(column)) for id_ in ids:
try: f = db.new_api.get_proxy_metadata(id_).get(column, None)
ids = db.search_getting_ids(query, None, use_virtual_library=use_vl != '0') if isinstance(f, (tuple, list)):
s = set() s.update(f)
for id_ in ids: elif f:
f = db.new_api.get_proxy_metadata(id_).get(column, None) s.add(str(f))
if isinstance(f, (tuple, list)): return sep.join(s)
s.update(f) except Exception as e:
elif f: raise ValueError(e)
s.add(str(f))
return sep.join(s)
except Exception as e:
raise ValueError(e)
_formatter_builtins = [ _formatter_builtins = [