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
This commit is contained in:
Charles Haley 2011-01-14 10:50:13 +00:00
parent 35f05e2866
commit eb0ebd4df0
4 changed files with 113 additions and 70 deletions

View File

@ -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.</p>
<p><code>evaluate(self, formatter, kwargs, mi, locals, your_arguments)
functions are usable. The function must be named <b>evaluate</b>, and
must have the signature shown below.</p>
<p><code>evaluate(self, formatter, kwargs, mi, locals, your parameters)
&rarr; returning a unicode string</code></p>
<p>The arguments to evaluate are:
<p>The parameters of the evaluate function are:
<ul>
<li><b>formatter:</b> the instance of the formatter being used to
<li><b>formatter</b>: the instance of the formatter being used to
evaluate the current template. You can use this to do recursive
template evaluation.</li>
<li><b>kwargs:</b> a dictionary of metadata. Field values are in this
dictionary. mi: a Metadata instance. Used to get field information.
<li><b>kwargs</b>: a dictionary of metadata. Field values are in this
dictionary.
<li><b>mi</b>: a Metadata instance. Used to get field information.
This parameter can be None in some cases, such as when evaluating
non-book templates.</li>
<li><b>locals:</b> the local variables assigned to by the current
<li><b>locals</b>: the local variables assigned to by the current
template program.</li>
<li><b>Your_arguments</b> 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.</li>
<li><b>your parameters</b>: 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.</li>
</ul></p>
<p>
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.
<pre>
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])
</pre>
if val:
return val
else:
return 'EMPTY'</pre>
This function can be called in any of the three template program modes:
<ul>
<li>single-function mode: {tags:my_ifempty()}</li>
<li>template program mode: {tags:'my_ifempty($)'}</li>
<li>general program mode: program: my_ifempty(field('tags'))</li>
</p>
''')
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

View File

@ -38,7 +38,7 @@
<item row="0" column="1">
<widget class="QComboBox" name="function_name">
<property name="toolTip">
<string>Enter the name of the function to create</string>
<string>Enter the name of the function to create.</string>
</property>
<property name="editable">
<bool>true</bool>
@ -48,7 +48,7 @@
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="toolTip">
<string></string>
<string/>
</property>
<property name="text">
<string>Arg &amp;count:</string>
@ -100,6 +100,13 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="replace_button">
<property name="text">
<string>Replace</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="create_button">
<property name="text">
@ -144,8 +151,7 @@
</layout>
</item>
<item row="0" column="0">
<widget class="QTextBrowser" name="textBrowser">
</widget>
<widget class="QTextBrowser" name="textBrowser"/>
</item>
</layout>
</widget>

View File

@ -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)

View File

@ -8,7 +8,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__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()