From eb0ebd4df04c58eac188db8a30f58d3c69ebc8f9 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Fri, 14 Jan 2011 10:50:13 +0000
Subject: [PATCH 1/2] Many small changes to the new formatter function stuff:
1) put isempty back. 2) use 'inspect' to get the source code for builtins so
it can be displayed in the managemnet dialog box 3) change 'eval' to 'eval_'
4) do a better job of formatting exceptions 5) many changes to the help text
6) added a 'replace' box to the management dialog, so it is visually clear
how to modify a function 7) got rid of a print statement
---
.../gui2/preferences/template_functions.py | 71 ++++++++++-----
.../gui2/preferences/template_functions.ui | 14 ++-
src/calibre/utils/formatter.py | 9 +-
src/calibre/utils/formatter_functions.py | 89 ++++++++++---------
4 files changed, 113 insertions(+), 70 deletions(-)
diff --git a/src/calibre/gui2/preferences/template_functions.py b/src/calibre/gui2/preferences/template_functions.py
index efcf9e6379..2e16b0f4c3 100644
--- a/src/calibre/gui2/preferences/template_functions.py
+++ b/src/calibre/gui2/preferences/template_functions.py
@@ -25,37 +25,49 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
template function is written in python. It takes information from the
book, processes it in some way, then returns a string result. Functions
defined here are usable in templates in the same way that builtin
- functions are usable. The function must be named evaluate, and must
- have the signature shown below.
- evaluate(self, formatter, kwargs, mi, locals, your_arguments)
+ functions are usable. The function must be named evaluate, and
+ must have the signature shown below.
+ evaluate(self, formatter, kwargs, mi, locals, your parameters)
→ returning a unicode string
- The arguments to evaluate are:
+
The parameters of the evaluate function are:
- - formatter: the instance of the formatter being used to
+
- formatter: the instance of the formatter being used to
evaluate the current template. You can use this to do recursive
template evaluation.
- - kwargs: a dictionary of metadata. Field values are in this
- dictionary. mi: a Metadata instance. Used to get field information.
+
- kwargs: a dictionary of metadata. Field values are in this
+ dictionary.
+
- mi: a Metadata instance. Used to get field information.
This parameter can be None in some cases, such as when evaluating
non-book templates.
- - locals: the local variables assigned to by the current
+
- locals: the local variables assigned to by the current
template program.
- - Your_arguments must be one or more parameter (number
- matching the arg count box), or the value *args for a variable number
- of arguments. These are values passed into the function. One argument
- is required, and is usually the value of the field being operated upon.
- Note that when writing in basic template mode, the user does not
- provide this first argument. Instead it is the value of the field the
- function is operating upon.
+ - your parameters: You must supply one or more formal
+ parameters. The number must match the arg count box, unless arg count is
+ -1 (variable number or arguments), in which case the last argument must
+ be *args. At least one argument is required, and is usually the value of
+ the field being operated upon. Note that when writing in basic template
+ mode, the user does not provide this first argument. Instead it is
+ supplied by the formatter.
- The following example function looks for various values in the tags
- metadata field, returning those values that appear in tags.
+ The following example function checks the value of the field. If the
+ field is not empty, the field's value is returned, otherwise the value
+ EMPTY is returned.
+ name: my_ifempty
+ arg count: 1
+ doc: my_ifempty(val) -- return val if it is not empty, otherwise the string 'EMPTY'
+ program code:
def evaluate(self, formatter, kwargs, mi, locals, val):
- awards=['allbooks', 'PBook', 'ggff']
- return ', '.join([t for t in kwargs.get('tags') if t in awards])
-
+ if val:
+ return val
+ else:
+ return 'EMPTY'
+ This function can be called in any of the three template program modes:
+
+ - single-function mode: {tags:my_ifempty()}
+ - template program mode: {tags:'my_ifempty($)'}
+ - general program mode: program: my_ifempty(field('tags'))
''')
self.textBrowser.setHtml(help_text)
@@ -67,14 +79,22 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.build_function_names_box()
self.function_name.currentIndexChanged[str].connect(self.function_index_changed)
self.function_name.editTextChanged.connect(self.function_name_edited)
+ self.argument_count.valueChanged.connect(self.enable_replace_button)
+ self.documentation.textChanged.connect(self.enable_replace_button)
+ self.program.textChanged.connect(self.enable_replace_button)
self.create_button.clicked.connect(self.create_button_clicked)
self.delete_button.clicked.connect(self.delete_button_clicked)
self.create_button.setEnabled(False)
self.delete_button.setEnabled(False)
+ self.replace_button.setEnabled(False)
self.clear_button.clicked.connect(self.clear_button_clicked)
+ self.replace_button.clicked.connect(self.replace_button_clicked)
self.program.setTabStopWidth(20)
self.highlighter = PythonHighlighter(self.program.document())
+ def enable_replace_button(self):
+ self.replace_button.setEnabled(self.delete_button.isEnabled())
+
def clear_button_clicked(self):
self.build_function_names_box()
self.program.clear()
@@ -112,6 +132,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.create_button.setEnabled(True)
self.delete_button.setEnabled(False)
self.build_function_names_box(set_to=name)
+ self.program.setReadOnly(False)
else:
error_dialog(self.gui, _('Template functions'),
_('Function not defined'), show=True)
@@ -143,6 +164,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.documentation.setReadOnly(False)
self.argument_count.setReadOnly(False)
self.create_button.setEnabled(True)
+ self.replace_button.setEnabled(False)
+ self.program.setReadOnly(False)
def function_index_changed(self, txt):
txt = unicode(txt)
@@ -156,15 +179,21 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
func = self.funcs[txt]
self.argument_count.setValue(func.arg_count)
self.documentation.setText(func.doc)
+ self.program.setPlainText(func.program_text)
if txt in self.builtins:
self.documentation.setReadOnly(True)
self.argument_count.setReadOnly(True)
- self.program.clear()
+ self.program.setReadOnly(True)
self.delete_button.setEnabled(False)
else:
self.program.setPlainText(func.program_text)
self.delete_button.setEnabled(True)
+ self.program.setReadOnly(False)
+ self.replace_button.setEnabled(False)
+ def replace_button_clicked(self):
+ self.delete_button_clicked()
+ self.create_button_clicked()
def refresh_gui(self, gui):
pass
diff --git a/src/calibre/gui2/preferences/template_functions.ui b/src/calibre/gui2/preferences/template_functions.ui
index d323a8dc7e..a1b4f82b14 100644
--- a/src/calibre/gui2/preferences/template_functions.ui
+++ b/src/calibre/gui2/preferences/template_functions.ui
@@ -38,7 +38,7 @@
-
- Enter the name of the function to create
+ Enter the name of the function to create.
true
@@ -48,7 +48,7 @@
-
-
+
Arg &count:
@@ -100,6 +100,13 @@
+ -
+
+
+ Replace
+
+
+
-
@@ -144,8 +151,7 @@
-
-
-
+
diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py
index abcb3021be..e48d274c08 100644
--- a/src/calibre/utils/formatter.py
+++ b/src/calibre/utils/formatter.py
@@ -96,7 +96,7 @@ class _Parser(object):
# classic assignment statement
self.consume()
cls = funcs['assign']
- return cls.eval(self.parent, self.parent.kwargs,
+ return cls.eval_(self.parent, self.parent.kwargs,
self.parent.book, self.parent.locals, id, self.expr())
return self.parent.locals.get(id, _('unknown id ') + id)
# We have a function.
@@ -130,7 +130,7 @@ class _Parser(object):
cls = funcs[id]
if cls.arg_count != -1 and len(args) != cls.arg_count:
self.error('incorrect number of arguments for function {}'.format(id))
- return cls.eval(self.parent, self.parent.kwargs,
+ return cls.eval_(self.parent, self.parent.kwargs,
self.parent.book, self.parent.locals, *args)
else:
f = self.parent.functions[id]
@@ -286,12 +286,11 @@ class TemplateFormatter(string.Formatter):
args = [self.backslash_comma_to_comma.sub(',', a) for a in args]
if (func.arg_count == 1 and (len(args) != 0)) or \
(func.arg_count > 1 and func.arg_count != len(args)+1):
- print args
raise ValueError('Incorrect number of arguments for function '+ fmt[0:p])
if func.arg_count == 1:
- val = func.eval(self, self.kwargs, self.book, self.locals, val).strip()
+ val = func.eval_(self, self.kwargs, self.book, self.locals, val).strip()
else:
- val = func.eval(self, self.kwargs, self.book, self.locals,
+ val = func.eval_(self, self.kwargs, self.book, self.locals,
val, *args).strip()
if val:
val = self._do_format(val, dispfmt)
diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py
index b0895ce1b3..7237f227e2 100644
--- a/src/calibre/utils/formatter_functions.py
+++ b/src/calibre/utils/formatter_functions.py
@@ -8,7 +8,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import re, traceback
+import inspect, re, traceback, sys
from calibre.utils.titlecase import titlecase
from calibre.utils.icu import capitalize, strcmp
@@ -58,13 +58,10 @@ class FormatterFunction(object):
name = 'no name provided'
arg_count = 0
- def __init__(self):
- formatter_functions.register_builtin(self)
-
def evaluate(self, formatter, kwargs, mi, locals, *args):
raise NotImplementedError()
- def eval(self, formatter, kwargs, mi, locals, *args):
+ def eval_(self, formatter, kwargs, mi, locals, *args):
try:
ret = self.evaluate(formatter, kwargs, mi, locals, *args)
if isinstance(ret, (str, unicode)):
@@ -75,9 +72,21 @@ class FormatterFunction(object):
return ','.join(list)
except:
traceback.print_exc()
- return _('Function threw exception' + traceback.format_exc())
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ info = ': '.join(traceback.format_exception(exc_type, exc_value,
+ exc_traceback)[-2:]).replace('\n', '')
+ return _('Exception ' + info)
-class BuiltinStrcmp(FormatterFunction):
+
+class BuiltinFormatterFunction(FormatterFunction):
+ def __init__(self):
+ formatter_functions.register_builtin(self)
+ eval_func = inspect.getmembers(self.__class__,
+ lambda x: inspect.ismethod(x) and x.__name__ == 'evaluate')
+ lines = [l[4:] for l in inspect.getsourcelines(eval_func[0][1])[0]]
+ self.program_text = ''.join(lines)
+
+class BuiltinStrcmp(BuiltinFormatterFunction):
name = 'strcmp'
arg_count = 5
doc = _('strcmp(x, y, lt, eq, gt) -- does a case-insensitive comparison of x '
@@ -92,7 +101,7 @@ class BuiltinStrcmp(FormatterFunction):
return eq
return gt
-class BuiltinCmp(FormatterFunction):
+class BuiltinCmp(BuiltinFormatterFunction):
name = 'cmp'
arg_count = 5
doc = _('cmp(x, y, lt, eq, gt) -- compares x and y after converting both to '
@@ -107,7 +116,7 @@ class BuiltinCmp(FormatterFunction):
return eq
return gt
-class BuiltinStrcat(FormatterFunction):
+class BuiltinStrcat(BuiltinFormatterFunction):
name = 'strcat'
arg_count = -1
doc = _('strcat(a, b, ...) -- can take any number of arguments. Returns a '
@@ -120,7 +129,7 @@ class BuiltinStrcat(FormatterFunction):
res += args[i]
return res
-class BuiltinAdd(FormatterFunction):
+class BuiltinAdd(BuiltinFormatterFunction):
name = 'add'
arg_count = 2
doc = _('add(x, y) -- returns x + y. Throws an exception if either x or y are not numbers.')
@@ -130,7 +139,7 @@ class BuiltinAdd(FormatterFunction):
y = float(y if y else 0)
return unicode(x + y)
-class BuiltinSubtract(FormatterFunction):
+class BuiltinSubtract(BuiltinFormatterFunction):
name = 'subtract'
arg_count = 2
doc = _('subtract(x, y) -- returns x - y. Throws an exception if either x or y are not numbers.')
@@ -140,7 +149,7 @@ class BuiltinSubtract(FormatterFunction):
y = float(y if y else 0)
return unicode(x - y)
-class BuiltinMultiply(FormatterFunction):
+class BuiltinMultiply(BuiltinFormatterFunction):
name = 'multiply'
arg_count = 2
doc = _('multiply(x, y) -- returns x * y. Throws an exception if either x or y are not numbers.')
@@ -150,7 +159,7 @@ class BuiltinMultiply(FormatterFunction):
y = float(y if y else 0)
return unicode(x * y)
-class BuiltinDivide(FormatterFunction):
+class BuiltinDivide(BuiltinFormatterFunction):
name = 'divide'
arg_count = 2
doc = _('divide(x, y) -- returns x / y. Throws an exception if either x or y are not numbers.')
@@ -160,7 +169,7 @@ class BuiltinDivide(FormatterFunction):
y = float(y if y else 0)
return unicode(x / y)
-class BuiltinTemplate(FormatterFunction):
+class BuiltinTemplate(BuiltinFormatterFunction):
name = 'template'
arg_count = 1
doc = _('template(x) -- evaluates x as a template. The evaluation is done '
@@ -168,17 +177,17 @@ class BuiltinTemplate(FormatterFunction):
'the caller and the template evaluation. Because the { and } '
'characters are special, you must use [[ for the { character and '
']] for the } character; they are converted automatically. '
- 'For example, ``template(\'[[title_sort]]\') will evaluate the '
+ 'For example, template(\'[[title_sort]]\') will evaluate the '
'template {title_sort} and return its value.')
def evaluate(self, formatter, kwargs, mi, locals, template):
template = template.replace('[[', '{').replace(']]', '}')
return formatter.safe_format(template, kwargs, 'TEMPLATE', mi)
-class BuiltinEval(FormatterFunction):
+class BuiltinEval(BuiltinFormatterFunction):
name = 'eval'
arg_count = 1
- doc = _('eval(template)`` -- evaluates the template, passing the local '
+ doc = _('eval(template) -- evaluates the template, passing the local '
'variables (those \'assign\'ed to) instead of the book metadata. '
' This permits using the template processor to construct complex '
'results from local variables.')
@@ -188,7 +197,7 @@ class BuiltinEval(FormatterFunction):
template = template.replace('[[', '{').replace(']]', '}')
return eval_formatter.safe_format(template, locals, 'EVAL', None)
-class BuiltinAssign(FormatterFunction):
+class BuiltinAssign(BuiltinFormatterFunction):
name = 'assign'
arg_count = 2
doc = _('assign(id, val) -- assigns val to id, then returns val. '
@@ -198,7 +207,7 @@ class BuiltinAssign(FormatterFunction):
locals[target] = value
return value
-class BuiltinPrint(FormatterFunction):
+class BuiltinPrint(BuiltinFormatterFunction):
name = 'print'
arg_count = -1
doc = _('print(a, b, ...) -- prints the arguments to standard output. '
@@ -209,7 +218,7 @@ class BuiltinPrint(FormatterFunction):
print args
return None
-class BuiltinField(FormatterFunction):
+class BuiltinField(BuiltinFormatterFunction):
name = 'field'
arg_count = 1
doc = _('field(name) -- returns the metadata field named by name')
@@ -217,7 +226,7 @@ class BuiltinField(FormatterFunction):
def evaluate(self, formatter, kwargs, mi, locals, name):
return formatter.get_value(name, [], kwargs)
-class BuiltinSubstr(FormatterFunction):
+class BuiltinSubstr(BuiltinFormatterFunction):
name = 'substr'
arg_count = 3
doc = _('substr(str, start, end) -- returns the start\'th through the end\'th '
@@ -230,7 +239,7 @@ class BuiltinSubstr(FormatterFunction):
def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_):
return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]
-class BuiltinLookup(FormatterFunction):
+class BuiltinLookup(BuiltinFormatterFunction):
name = 'lookup'
arg_count = -1
doc = _('lookup(val, pattern, field, pattern, field, ..., else_field) -- '
@@ -257,7 +266,7 @@ class BuiltinLookup(FormatterFunction):
return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs)
i += 2
-class BuiltinTest(FormatterFunction):
+class BuiltinTest(BuiltinFormatterFunction):
name = 'test'
arg_count = 3
doc = _('test(val, text if not empty, text if empty) -- return `text if not '
@@ -269,7 +278,7 @@ class BuiltinTest(FormatterFunction):
else:
return value_not_set
-class BuiltinContains(FormatterFunction):
+class BuiltinContains(BuiltinFormatterFunction):
name = 'contains'
arg_count = 4
doc = _('contains(val, pattern, text if match, text if not match) -- checks '
@@ -284,13 +293,13 @@ class BuiltinContains(FormatterFunction):
else:
return value_if_not
-class BuiltinSwitch(FormatterFunction):
+class BuiltinSwitch(BuiltinFormatterFunction):
name = 'switch'
arg_count = -1
doc = _('switch(val, pattern, value, pattern, value, ..., else_value) -- '
- 'for each ``pattern, value`` pair, checks if the field matches '
- 'the regular expression ``pattern`` and if so, returns that '
- 'value. If no pattern matches, then else_value is returned. '
+ 'for each `pattern, value` pair, checks if the field matches '
+ 'the regular expression `pattern` and if so, returns that '
+ '`value`. If no pattern matches, then else_value is returned. '
'You can have as many `pattern, value` pairs as you want')
def evaluate(self, formatter, kwargs, mi, locals, val, *args):
@@ -304,7 +313,7 @@ class BuiltinSwitch(FormatterFunction):
return args[i+1]
i += 2
-class BuiltinRe(FormatterFunction):
+class BuiltinRe(BuiltinFormatterFunction):
name = 're'
arg_count = 3
doc = _('re(val, pattern, replacement) -- return the field after applying '
@@ -315,10 +324,10 @@ class BuiltinRe(FormatterFunction):
def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):
return re.sub(pattern, replacement, val)
-class BuiltinEvaluate(FormatterFunction):
- name = 'evaluate'
+class BuiltinIfempty(BuiltinFormatterFunction):
+ name = 'ifempty'
arg_count = 2
- doc = _('evaluate(val, text if empty) -- return val if val is not empty, '
+ doc = _('ifempty(val, text if empty) -- return val if val is not empty, '
'otherwise return `text if empty`')
def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty):
@@ -327,7 +336,7 @@ class BuiltinEvaluate(FormatterFunction):
else:
return value_if_empty
-class BuiltinShorten(FormatterFunction):
+class BuiltinShorten(BuiltinFormatterFunction):
name = 'shorten'
arg_count = 4
doc = _('shorten(val, left chars, middle text, right chars) -- Return a '
@@ -352,7 +361,7 @@ class BuiltinShorten(FormatterFunction):
else:
return val
-class BuiltinCount(FormatterFunction):
+class BuiltinCount(BuiltinFormatterFunction):
name = 'count'
arg_count = 2
doc = _('count(val, separator) -- interprets the value as a list of items '
@@ -363,7 +372,7 @@ class BuiltinCount(FormatterFunction):
def evaluate(self, formatter, kwargs, mi, locals, val, sep):
return unicode(len(val.split(sep)))
-class BuiltinListitem(FormatterFunction):
+class BuiltinListitem(BuiltinFormatterFunction):
name = 'list_item'
arg_count = 3
doc = _('list_item(val, index, separator) -- interpret the value as a list of '
@@ -383,7 +392,7 @@ class BuiltinListitem(FormatterFunction):
except:
return ''
-class BuiltinUppercase(FormatterFunction):
+class BuiltinUppercase(BuiltinFormatterFunction):
name = 'uppercase'
arg_count = 1
doc = _('uppercase(val) -- return value of the field in upper case')
@@ -391,7 +400,7 @@ class BuiltinUppercase(FormatterFunction):
def evaluate(self, formatter, kwargs, mi, locals, val):
return val.upper()
-class BuiltinLowercase(FormatterFunction):
+class BuiltinLowercase(BuiltinFormatterFunction):
name = 'lowercase'
arg_count = 1
doc = _('lowercase(val) -- return value of the field in lower case')
@@ -399,7 +408,7 @@ class BuiltinLowercase(FormatterFunction):
def evaluate(self, formatter, kwargs, mi, locals, val):
return val.lower()
-class BuiltinTitlecase(FormatterFunction):
+class BuiltinTitlecase(BuiltinFormatterFunction):
name = 'titlecase'
arg_count = 1
doc = _('titlecase(val) -- return value of the field in title case')
@@ -407,7 +416,7 @@ class BuiltinTitlecase(FormatterFunction):
def evaluate(self, formatter, kwargs, mi, locals, val):
return titlecase(val)
-class BuiltinCapitalize(FormatterFunction):
+class BuiltinCapitalize(BuiltinFormatterFunction):
name = 'capitalize'
arg_count = 1
doc = _('capitalize(val) -- return value of the field capitalized')
@@ -423,7 +432,7 @@ builtin_contains = BuiltinContains()
builtin_count = BuiltinCount()
builtin_divide = BuiltinDivide()
builtin_eval = BuiltinEval()
-builtin_evaluate = BuiltinEvaluate()
+builtin_ifempty = BuiltinIfempty()
builtin_field = BuiltinField()
builtin_list_item = BuiltinListitem()
builtin_lookup = BuiltinLookup()
From 44e2e66c2c71db9b768a29bd7793647ff42429cf Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Fri, 14 Jan 2011 12:27:23 +0000
Subject: [PATCH 2/2] Fix regression in argument counting in the formatter
---
src/calibre/utils/formatter.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py
index e48d274c08..49b807ff1c 100644
--- a/src/calibre/utils/formatter.py
+++ b/src/calibre/utils/formatter.py
@@ -284,7 +284,7 @@ class TemplateFormatter(string.Formatter):
else:
args = self.arg_parser.scan(fmt[p+1:])[0]
args = [self.backslash_comma_to_comma.sub(',', a) for a in args]
- if (func.arg_count == 1 and (len(args) != 0)) or \
+ if (func.arg_count == 1 and (len(args) != 1 or args[0])) or \
(func.arg_count > 1 and func.arg_count != len(args)+1):
raise ValueError('Incorrect number of arguments for function '+ fmt[0:p])
if func.arg_count == 1: