This commit is contained in:
Kovid Goyal 2022-10-11 22:22:47 +05:30
commit 7c7aef46f6
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 76 additions and 34 deletions

View File

@ -654,25 +654,31 @@ A PTM template begins with:
.. code-block:: python .. code-block:: python
python: python:
def evaluate(book, db, globals, arguments, **kwargs): def evaluate(book, context):
# book is a calibre metadata object # book is a calibre metadata object
# db is a calibre legacy database object # context is an instance of calibre.utils.formatter.PythonTemplateContext,
# globals is the template global variable dictionary # which (currently) contains the following attributes:
# arguments is a list of arguments if the template is called by a GPM template, otherwise None # db: a calibre legacy database object
# kwargs is a dictionary provided for future use # globals: the template global variable dictionary
# arguments: is a list of arguments if the template is called by a GPM template, otherwise None
# Python code goes here # your Python code goes here
return 'a string' return 'a string'
You can add the above text to your template using the context menu, usually accessed with a right click. The comments are not significant and can be removed. You must use python indenting. You can add the above text to your template using the context menu, usually accessed with a right click. The comments are not significant and can be removed. You must use python indenting.
Here is an example of a PTM template that produces a list of all the authors for a series. The list is stored in a `Column built from other columns, behaves like tags`. It shows in :guilabel:`Book details` and has the :guilabel:`on separate lines` checked (in :guilabel:`Preferences->Look & feel->Book details`). That option requires the list to be comma-separated. To satisfy that requirement the template converts commas in author names to semicolons then builds a comma-separated list of authors. The authors are then sorted, which is why the template uses author_sort.:: The context object supports ``str(context)`` that returns a string of the context's contents, and ``context.attributes`` that returns a list of the attribute names in the context.
Here is an example of a PTM template that produces a list of all the authors for a series. The list is stored in a `Column built from other columns, behaves like tags`. It shows in :guilabel:`Book details` and has the :guilabel:`on separate lines` checked (in :guilabel:`Preferences->Look & feel->Book details`). That option requires the list to be comma-separated. To satisfy that requirement the template converts commas in author names to semicolons then builds a comma-separated list of authors. The authors are then sorted, which is why the template uses author_sort.
.. code-block:: python
python: python:
def evaluate(book, db, globals, arguments, **kwargs): def evaluate(book, context):
if book.series is None: if book.series is None:
return '' return ''
ans = set() ans = set()
db = context.db
for id_ in db.search_getting_ids(f'series:"={book.series}"', ''): for id_ in db.search_getting_ids(f'series:"={book.series}"', ''):
ans.update(v.strip() for v in db.new_api.field_for('author_sort', id_).split('&')) ans.update(v.strip() for v in db.new_api.field_for('author_sort', id_).split('&'))
return ', '.join(v.replace(',', ';') for v in sorted(ans)) return ', '.join(v.replace(',', ';') for v in sorted(ans))

View File

@ -822,8 +822,8 @@ class ReadingTest(BaseTest):
# test counting books matching a search # test counting books matching a search
template = '''python: template = '''python:
def evaluate(book, db, **kwargs): def evaluate(book, ctx):
ids = db.new_api.search("series:true") ids = ctx.db.new_api.search("series:true")
return str(len(ids)) return str(len(ids))
''' '''
v = formatter.safe_format(template, {}, 'TEMPLATE ERROR', mi) v = formatter.safe_format(template, {}, 'TEMPLATE ERROR', mi)
@ -831,8 +831,8 @@ def evaluate(book, db, **kwargs):
# test counting books when none match the search # test counting books when none match the search
template = '''python: template = '''python:
def evaluate(book, db, **kwargs): def evaluate(book, ctx):
ids = db.new_api.search("series:afafaf") ids = ctx.db.new_api.search("series:afafaf")
return str(len(ids)) return str(len(ids))
''' '''
v = formatter.safe_format(template, {}, 'TEMPLATE ERROR', mi) v = formatter.safe_format(template, {}, 'TEMPLATE ERROR', mi)
@ -840,8 +840,8 @@ def evaluate(book, db, **kwargs):
# test is_multiple values # test is_multiple values
template = '''python: template = '''python:
def evaluate(book, db, **kwargs): def evaluate(book, ctx):
tags = db.new_api.all_field_names('tags') tags = ctx.db.new_api.all_field_names('tags')
return ','.join(list(tags)) return ','.join(list(tags))
''' '''
v = formatter.safe_format(template, {}, 'TEMPLATE ERROR', mi) v = formatter.safe_format(template, {}, 'TEMPLATE ERROR', mi)
@ -855,9 +855,9 @@ def evaluate(book, db, **kwargs):
"", "",
0, 0,
'''python: '''python:
def evaluate(book, db, globals, arguments): def evaluate(book, ctx):
tags = set(db.new_api.all_field_names('tags')) tags = set(ctx.db.new_api.all_field_names('tags'))
tags.add(arguments[0]) tags.add(ctx.arguments[0])
return ','.join(list(tags)) return ','.join(list(tags))
''' '''
]], None) ]], None)

View File

@ -595,17 +595,18 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
m.exec(self.textbox.mapToGlobal(point)) m.exec(self.textbox.mapToGlobal(point))
def add_python_template_header_text(self): def add_python_template_header_text(self):
self.textbox.setPlainText('python:\n' self.textbox.setPlainText('''python:
'def evaluate(book, db, globals, arguments, **kwargs):\n' def evaluate(book, context):
'\t# book is a calibre metadata object\n' # book is a calibre metadata object
'\t# db is a calibre legacy database object\n' # context is an instance of calibre.utils.formatter.PythonTemplateContext,
'\t# globals is the template global variable dictionary\n' # which currently contains the following attributes:
'\t# arguments is a list of arguments if the template is ' # db: a calibre legacy database object
'called by a GPM template, otherwise None\n' # globals: the template global variable dictionary
'\t# kwargs is a dictionary provided for future use' # arguments: is a list of arguments if the template is called by a GPM template, otherwise None
'\n\n\t# Python code goes here\n'
"\treturn 'a string'" + # your Python code goes here
self.textbox.toPlainText()) return 'a string'
''')
def set_word_wrap(self, to_what): def set_word_wrap(self, to_what):
gprefs['gpm_template_editor_word_wrap_mode'] = to_what gprefs['gpm_template_editor_word_wrap_mode'] = to_what

View File

@ -3,12 +3,12 @@ Created on 23 Sep 2010
@author: charles @author: charles
''' '''
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import re, string, traceback, numbers import re, string, traceback, numbers
from collections import OrderedDict
from functools import partial from functools import partial
from math import modf from math import modf
from sys import exc_info from sys import exc_info
@ -830,6 +830,40 @@ class StopException(Exception):
super().__init__('Template evaluation stopped') super().__init__('Template evaluation stopped')
class PythonTemplateContext(object):
def __init__(self, **kwargs):
# Set attributes we already know must exist.
self.db = None
self.arguments = None
self.globals = None
attrs_set = {'db', 'arguments', 'globals'}
# Create/set attributes from the named parameters. Doing it this way we
# aren't required to change the signature of __init__ if/when we add
# attributes in the future. However, if a user depends upon the
# existence of some attribute and the context creator doesn't supply it
# then the user will get an AttributeError exception.
for k,v in kwargs.items():
attrs_set.add(k)
setattr(self, k, v)
self.attrs_set = attrs_set
@property
def attributes(self):
# return a list of attributes in the context object
return sorted(list(self.attrs_set))
def __str__(self):
# return a string of the attribute with values separated by newlines
attrs = sorted(list(self.attrs_set))
ans = OrderedDict()
for k in attrs:
ans[k] = getattr(self, k, None)
return '\n'.join(f'{k}:{v}' for k,v in ans.items())
class _Interpreter: class _Interpreter:
def error(self, message, line_number): def error(self, message, line_number):
m = _('Interpreter: {0} - line number {1}').format(message, line_number) m = _('Interpreter: {0} - line number {1}').format(message, line_number)
@ -1564,10 +1598,11 @@ class TemplateFormatter(string.Formatter):
def _run_python_template(self, compiled_template, arguments): def _run_python_template(self, compiled_template, arguments):
try: try:
return compiled_template(book=self.book, return compiled_template(self.book,
db=get_database(self.book, get_database(self.book, None)), PythonTemplateContext(
globals=self.global_vars, db=get_database(self.book, get_database(self.book, None)),
arguments=arguments) globals=self.global_vars,
arguments=arguments))
except Exception as e: except Exception as e:
ss = traceback.extract_tb(exc_info()[2])[-1] ss = traceback.extract_tb(exc_info()[2])[-1]
raise ValueError(_('Error in function {0} on line {1} : {2} - {3}').format( raise ValueError(_('Error in function {0} on line {1} : {2} - {3}').format(