mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge branch 'master' of https://github.com/cbhaley/calibre
This commit is contained in:
commit
3abac8e7bb
@ -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 = {}
|
||||
|
@ -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
|
||||
|
@ -754,66 +754,55 @@ class ReadingTest(BaseTest):
|
||||
db = self.init_cache(self.library_path)
|
||||
db.create_custom_column('mult', 'CC1', 'composite', True, display={'composite_template': 'b,a,c'})
|
||||
|
||||
# need an empty metadata object to pass to the formatter
|
||||
db = self.init_legacy(self.library_path)
|
||||
mi = db.get_metadata(1)
|
||||
|
||||
class GetGuiAns():
|
||||
current_db = None
|
||||
get_gui_ans = GetGuiAns()
|
||||
get_gui_ans.current_db = db
|
||||
from calibre.gui2.ui import get_gui
|
||||
get_gui.ans = get_gui_ans
|
||||
# test counting books matching the search
|
||||
v = formatter.safe_format('program: book_count("series:true", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(v, '2')
|
||||
|
||||
try:
|
||||
# need an empty metadata object to pass to the formatter
|
||||
mi = Metadata('A', 'B')
|
||||
# test counting books when none match the search
|
||||
v = formatter.safe_format('program: book_count("series:afafaf", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(v, '0')
|
||||
|
||||
# test counting books matching the search
|
||||
v = formatter.safe_format('program: book_count("series:true", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(v, '2')
|
||||
# test is_multiple values
|
||||
v = formatter.safe_format('program: book_values("tags", "tags:true", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(set(v.split(',')), {'Tag One', 'News', 'Tag Two'})
|
||||
|
||||
# test counting books when none match the search
|
||||
v = formatter.safe_format('program: book_count("series:afafaf", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(v, '0')
|
||||
# test not is_multiple values
|
||||
v = formatter.safe_format('program: book_values("series", "series:true", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(v, 'A Series One')
|
||||
|
||||
# test is_multiple values
|
||||
v = formatter.safe_format('program: book_values("tags", "tags:true", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(set(v.split(',')), {'Tag One', 'News', 'Tag Two'})
|
||||
# test returning values for a column not searched for
|
||||
v = formatter.safe_format('program: book_values("tags", "series:\\"A Series One\\"", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(set(v.split(',')), {'Tag One', 'News', 'Tag Two'})
|
||||
|
||||
# test not is_multiple values
|
||||
v = formatter.safe_format('program: book_values("series", "series:true", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(v, 'A Series One')
|
||||
# test getting a singleton value from books where the column is empty
|
||||
v = formatter.safe_format('program: book_values("series", "series:false", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(v, '')
|
||||
|
||||
# test returning values for a column not searched for
|
||||
v = formatter.safe_format('program: book_values("tags", "series:\\"A Series One\\"", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(set(v.split(',')), {'Tag One', 'News', 'Tag Two'})
|
||||
# test getting a multiple value from books where the column is empty
|
||||
v = formatter.safe_format('program: book_values("tags", "tags:false", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(v, '')
|
||||
|
||||
# test getting a singleton value from books where the column is empty
|
||||
v = formatter.safe_format('program: book_values("series", "series:false", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(v, '')
|
||||
# test fetching an unknown column
|
||||
v = formatter.safe_format('program: book_values("taaags", "tags:false", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(v, "TEMPLATE ERROR The column taaags doesn't exist")
|
||||
|
||||
# test getting a multiple value from books where the column is empty
|
||||
v = formatter.safe_format('program: book_values("tags", "tags:false", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(v, '')
|
||||
# test finding all books
|
||||
v = formatter.safe_format('program: book_values("id", "title:true", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(set(v.split(',')), {'1', '2', '3'})
|
||||
|
||||
# test fetching an unknown column
|
||||
v = formatter.safe_format('program: book_values("taaags", "tags:false", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(v, "TEMPLATE ERROR The column taaags doesn't exist")
|
||||
# test getting value of a composite
|
||||
v = formatter.safe_format('program: book_values("#mult", "id:1", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(set(v.split(',')), {'b', 'c', 'a'})
|
||||
|
||||
# test finding all books
|
||||
v = formatter.safe_format('program: book_values("id", "title:true", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(set(v.split(',')), {'1', '2', '3'})
|
||||
# test getting value of a custom float
|
||||
v = formatter.safe_format('program: book_values("#float", "title:true", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(set(v.split(',')), {'20.02', '10.01'})
|
||||
|
||||
# test getting value of a composite
|
||||
v = formatter.safe_format('program: book_values("#mult", "id:1", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(set(v.split(',')), {'b', 'c', 'a'})
|
||||
|
||||
# test getting value of a custom float
|
||||
v = formatter.safe_format('program: book_values("#float", "title:true", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(set(v.split(',')), {'20.02', '10.01'})
|
||||
|
||||
# test getting value of an int (rating)
|
||||
v = formatter.safe_format('program: book_values("rating", "title:true", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(set(v.split(',')), {'4', '6'})
|
||||
finally:
|
||||
get_gui.ans = None
|
||||
# test getting value of an int (rating)
|
||||
v = formatter.safe_format('program: book_values("rating", "title:true", ",", 0)', {}, 'TEMPLATE ERROR', mi)
|
||||
self.assertEqual(set(v.split(',')), {'4', '6'})
|
||||
# }}}
|
||||
|
@ -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)
|
||||
|
@ -12,10 +12,10 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__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,27 @@ 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))
|
||||
|
||||
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))
|
||||
return db
|
||||
|
||||
|
||||
class BuiltinFormatterFunction(FormatterFunction):
|
||||
|
||||
@ -928,7 +951,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 +1259,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 +1278,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 +1290,8 @@ 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)
|
||||
return '' if c == 0 else str(c)
|
||||
return _('This function can be used only in the GUI')
|
||||
c = self.get_database(mi).new_api.annotation_count_for_book(mi.id)
|
||||
return '' if c == 0 else str(c)
|
||||
|
||||
|
||||
class BuiltinIsMarked(BuiltinFormatterFunction):
|
||||
@ -1284,11 +1304,8 @@ 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)
|
||||
return c if c else ''
|
||||
return _('This function can be used only in the GUI')
|
||||
c = self.get_database(mi).data.get_marked(mi.id)
|
||||
return c if c else ''
|
||||
|
||||
|
||||
class BuiltinSeriesSort(BuiltinFormatterFunction):
|
||||
@ -1850,14 +1867,12 @@ class BuiltinVirtualLibraries(BuiltinFormatterFunction):
|
||||
'column\'s value in your save/send templates')
|
||||
|
||||
def evaluate(self, formatter, kwargs, mi, locals_):
|
||||
with suppress(Exception):
|
||||
try:
|
||||
from calibre.gui2.ui import get_gui
|
||||
a = get_gui().current_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')
|
||||
db = self.get_database(mi)
|
||||
try:
|
||||
a = db.data.get_virtual_libraries_for_books((mi.id,))
|
||||
return ', '.join(a[mi.id])
|
||||
except ValueError as v:
|
||||
return str(v)
|
||||
|
||||
|
||||
class BuiltinCurrentVirtualLibraryName(BuiltinFormatterFunction):
|
||||
@ -1870,10 +1885,7 @@ 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')
|
||||
return self.get_database(mi).data.get_base_restriction_name()
|
||||
|
||||
|
||||
class BuiltinUserCategories(BuiltinFormatterFunction):
|
||||
@ -1893,7 +1905,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 +1946,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 +1982,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 +2003,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 +2018,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 +2039,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 +2060,9 @@ 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
|
||||
if field not in self.get_database(mi).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 +2258,12 @@ 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):
|
||||
try:
|
||||
from calibre.gui2.ui import get_gui
|
||||
ids = get_gui().current_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')
|
||||
db = self.get_database(mi)
|
||||
try:
|
||||
ids = db.search_getting_ids(query, None, use_virtual_library=use_vl != '0')
|
||||
return len(ids)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class BuiltinBookValues(BuiltinFormatterFunction):
|
||||
@ -2265,11 +2281,7 @@ 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')
|
||||
db = self.get_database(mi)
|
||||
if column not in db.field_metadata:
|
||||
raise ValueError(_("The column {} doesn't exist").format(column))
|
||||
try:
|
||||
|
Loading…
x
Reference in New Issue
Block a user