Thank you @capink & @un_pogaz for your comments. I think I have something that will satisfy you both.
What is in this implemention:
- A complete implementation, including caching
- It no longer uses a class, instead a standard function definition. The globals dict provides a better way to persist data than class attributes, and helper functions can still be defined as part of the template.
- In addition to standalone python templates you can create stored python templates. This raised the issue @un_pogaz was discussing: passing arguments to a python template. If you call a stored template (either GPM or python) from a GPM or SFM template then you can pass positional arguments. A standalone python template cannot be given args and the 'arguments' parameter is None.
- Python stored templates are callable in all template language modes.
Python templates have the following header that you can insert using a context menu line in the template editor.
Code:
python:
def evaluate(book, db, globals, arguments, **kwargs):
# 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
# Python code goes here
return 'a string'
As you can see I ended up using positional parameters for the arguments that won't change, and providing a kwargs dict for future proofing.
One sample template:
Code:
python:
def evaluate(book, db, globals, arguments, **kwargs):
print(book.authors)
print('aaa', db.path(book.id, index_is_id=True))
s = get_default_if_none(book, 'series', '**no series**')
return s + ':::' + str(get_default_if_none(book, '#myint', 999))
def get_default_if_none(book, field, default):
v = book.get(field, None)
return default if v is None else v
Here is a sample of using a stored python template. First the stored template:
Code:
python:
def evaluate(book, db, globals, arguments, **kwargs):
if arguments:
if len(arguments != 2):
return 'incorrect number of arguments'
return arguments[0] + ':::' + arguments[1]
return 'no arguments provided'
Here is a screen capture of the stored template definition dialog.
The sample calling GPM template:
Code:
program:
a_python_template_2_args(1, 'aaaa')
I know of one possible incompatibility. Adding python stored templates introduced a third stored object type. The three are user formatter functions, stored GPM templates, and stored python templates. I replaced the existing mechanism, an bool named is_python, with an enumeration. Code that cares about the stored template type must look at the 'object_type' attribute of the FormatterUserFunction instance. For compatibility, is_python is set to True for user formatter functions, false otherwise.
@capink: you use the FormatterFunction class in Action Chains and set is_python=True. This works with the new code to distinguish between python template functions and stored templates. However, if you need to distinguish between stored GPM templates and stored python templates then you must use the object_type attribute. If you want to store python templates then you must use the FormatterUserFunction class instead of the FormatterFunction class.
The current code is attached. There are 5 .py files. If either of you have time and energy to try it before I submit the changes to Kovid that would be great.
EDIT: I removed this zip file. It had the files in the wrong place.
A new file is in this post.