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
python:
def evaluate(book, db, globals, arguments, **kwargs):
def evaluate(book, context):
# book is a calibre metadata object
# db is a calibre legacy database object
# globals is the template global variable dictionary
# arguments is a list of arguments if the template is called by a GPM template, otherwise None
# kwargs is a dictionary provided for future use
# context is an instance of calibre.utils.formatter.PythonTemplateContext,
# which (currently) contains the following attributes:
# db: a calibre legacy database object
# 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
return 'a string'
# your Python code goes here
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.
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:
def evaluate(book, db, globals, arguments, **kwargs):
def evaluate(book, context):
if book.series is None:
return ''
ans = set()
db = context.db
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('&'))
return ', '.join(v.replace(',', ';') for v in sorted(ans))

View File

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

View File

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

View File

@ -3,12 +3,12 @@ Created on 23 Sep 2010
@author: charles
'''
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import re, string, traceback, numbers
from collections import OrderedDict
from functools import partial
from math import modf
from sys import exc_info
@ -830,6 +830,40 @@ class StopException(Exception):
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:
def error(self, 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):
try:
return compiled_template(book=self.book,
db=get_database(self.book, get_database(self.book, None)),
globals=self.global_vars,
arguments=arguments)
return compiled_template(self.book,
PythonTemplateContext(
db=get_database(self.book, get_database(self.book, None)),
globals=self.global_vars,
arguments=arguments))
except Exception as e:
ss = traceback.extract_tb(exc_info()[2])[-1]
raise ValueError(_('Error in function {0} on line {1} : {2} - {3}').format(