Merge branch 'StoredTemplates'

This commit is contained in:
Charles Haley 2020-09-28 12:40:35 +01:00
commit 09b56dd006
8 changed files with 427 additions and 30 deletions

View File

@ -935,6 +935,12 @@ class ActionTemplateTester(InterfaceActionBase):
description = _('Show an editor for testing templates') description = _('Show an editor for testing templates')
class ActionStoredTemplates(InterfaceActionBase):
name = 'Stored Templates'
actual_plugin = 'calibre.gui2.actions.show_stored_templates:ShowStoredTemplatesAction'
description = _('Show a dialog for creating and managing stored templates')
class ActionSaveToDisk(InterfaceActionBase): class ActionSaveToDisk(InterfaceActionBase):
name = 'Save To Disk' name = 'Save To Disk'
actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction' actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction'
@ -1099,7 +1105,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
ActionCopyToLibrary, ActionTweakEpub, ActionUnpackBook, ActionNextMatch, ActionStore, ActionCopyToLibrary, ActionTweakEpub, ActionUnpackBook, ActionNextMatch, ActionStore,
ActionPluginUpdater, ActionPickRandom, ActionEditToC, ActionSortBy, ActionPluginUpdater, ActionPickRandom, ActionEditToC, ActionSortBy,
ActionMarkBooks, ActionEmbed, ActionTemplateTester, ActionTagMapper, ActionAuthorMapper, ActionMarkBooks, ActionEmbed, ActionTemplateTester, ActionTagMapper, ActionAuthorMapper,
ActionVirtualLibrary, ActionBrowseAnnotations] ActionVirtualLibrary, ActionBrowseAnnotations, ActionStoredTemplates]
# }}} # }}}
@ -1272,6 +1278,18 @@ class TemplateFunctions(PreferencesPlugin):
description = _('Create your own template functions') description = _('Create your own template functions')
class StoredTemplates(PreferencesPlugin):
name = 'StoredTemplates'
icon = I('template_funcs.png')
gui_name = _('Stored templates')
category = 'Advanced'
gui_category = _('Advanced')
category_order = 5
name_order = 6
config_widget = 'calibre.gui2.preferences.stored_templates'
description = _('Create stored calibre templates')
class Email(PreferencesPlugin): class Email(PreferencesPlugin):
name = 'Email' name = 'Email'
icon = I('mail.png') icon = I('mail.png')
@ -1376,7 +1394,7 @@ class Misc(PreferencesPlugin):
plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions, plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard, CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard,
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions, Email, Server, Plugins, Tweaks, Misc, TemplateFunctions,
MetadataSources, Keyboard, IgnoredDevices] StoredTemplates, MetadataSources, Keyboard, IgnoredDevices]
# }}} # }}}

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.preferences.main import Preferences
class ShowStoredTemplatesAction(InterfaceAction):
name = 'Stored Template'
action_spec = (_('Stored Templates'), 'debug.png', None, ())
dont_add_to = frozenset(('context-menu-device',))
action_type = 'current'
def genesis(self):
self.previous_text = _('Manage stored templates')
self.first_time = True
self.qaction.triggered.connect(self.show_template_editor)
def show_template_editor(self, *args):
d = Preferences(self.gui, initial_plugin=('Advanced', 'StoredTemplates'),
close_after_initial=True)
d.exec_()

View File

@ -429,12 +429,17 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
name = unicode_type(toWhat) name = unicode_type(toWhat)
self.source_code.clear() self.source_code.clear()
self.documentation.clear() self.documentation.clear()
self.func_type.clear()
if name in self.funcs: if name in self.funcs:
self.documentation.setPlainText(self.funcs[name].doc) self.documentation.setPlainText(self.funcs[name].doc)
if name in self.builtins and name in self.builtin_source_dict: if name in self.builtins and name in self.builtin_source_dict:
self.source_code.setPlainText(self.builtin_source_dict[name]) self.source_code.setPlainText(self.builtin_source_dict[name])
else: else:
self.source_code.setPlainText(self.funcs[name].program_text) self.source_code.setPlainText(self.funcs[name].program_text)
if self.funcs[name].is_python:
self.func_type.setText(_('Template function in python'))
else:
self.func_type.setText(_('Stored template'))
def accept(self): def accept(self):
txt = unicode_type(self.textbox.toPlainText()).rstrip() txt = unicode_type(self.textbox.toPlainText()).rstrip()

View File

@ -224,6 +224,26 @@
<widget class="QComboBox" name="function"/> <widget class="QComboBox" name="function"/>
</item> </item>
<item row="9" column="0"> <item row="9" column="0">
<widget class="QLabel" name="label_22">
<property name="text">
<string>&amp;Function type:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="buddy">
<cstring>func_type</cstring>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QLineEdit" name="func_type">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_2"> <widget class="QLabel" name="label_2">
<property name="text"> <property name="text">
<string>&amp;Documentation:</string> <string>&amp;Documentation:</string>
@ -236,10 +256,10 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="10" column="0"> <item row="11" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
<string>Python &amp;code:</string> <string>&amp;Code:</string>
</property> </property>
<property name="alignment"> <property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
@ -249,7 +269,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="9" column="1"> <item row="10" column="1">
<widget class="QPlainTextEdit" name="documentation"> <widget class="QPlainTextEdit" name="documentation">
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
@ -259,17 +279,17 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="10" column="1"> <item row="11" column="1">
<widget class="QPlainTextEdit" name="source_code"/> <widget class="QPlainTextEdit" name="source_code"/>
</item> </item>
<item row="11" column="1"> <item row="12" column="1">
<widget class="QLabel" name="template_tutorial"> <widget class="QLabel" name="template_tutorial">
<property name="openExternalLinks"> <property name="openExternalLinks">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="12" column="1"> <item row="13" column="1">
<widget class="QLabel" name="template_func_reference"> <widget class="QLabel" name="template_func_reference">
<property name="openExternalLinks"> <property name="openExternalLinks">
<bool>true</bool> <bool>true</bool>

View File

@ -0,0 +1,212 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import traceback
from PyQt5.Qt import (Qt, QGridLayout, QLabel, QSpacerItem, QSizePolicy,
QComboBox, QTextEdit, QHBoxLayout, QPushButton)
from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.utils.formatter_functions import (formatter_functions,
compile_user_function, compile_user_template_functions,
load_user_template_functions, function_pref_is_python,
function_pref_name)
from polyglot.builtins import native_string_type, unicode_type
class ConfigWidget(ConfigWidgetBase):
def genesis(self, gui):
self.gui = gui
self.db = gui.library_view.model().db
help_text = '<p>' + _('''
Here you can add and remove stored templates used in template processing.
You use a stored template in another template with the 'call' template
function, as in 'call(somename, arguments...). Stored templates must use
General Program Mode -- they must begin with the text 'program:'.
In the stored template you get the arguments using the 'arguments()'
template function, as in arguments(var1, var2, ...). The calling arguments
are copied to the named variables.
''') + '</p>'
l = QGridLayout(self)
w = QLabel(help_text)
w.setWordWrap(True)
l.addWidget(w, 0, 0, 1, 2)
lab = QLabel(_('&Template'))
l.addWidget(lab, 1, 0)
lb = QHBoxLayout()
self.program = w = TemplateLineEditor(self)
w.setPlaceholderText(_('The GPM template.'))
lab.setBuddy(w)
lb.addWidget(w, stretch=1)
self.editor_button = b = QPushButton(_('&Open Editor'))
b.clicked.connect(w.open_editor)
lb.addWidget(b)
l.addLayout(lb, 1, 1)
lab = QLabel(_('&Name'))
l.addWidget(lab, 2, 0)
self.function_name = w = QComboBox(self)
w.setEditable(True)
lab.setBuddy(w)
w.setToolTip(_('The name of the function, used in a call statement'))
l.addWidget(w, 2, 1)
lab = QLabel(_('&Documentation'))
l.addWidget(lab, 3, 0, Qt.AlignTop)
self.documentation = w = QTextEdit(self)
w.setPlaceholderText(_('A description of the template. Whatever you wish ...'))
lab.setBuddy(w)
l.addWidget(w, 3, 1)
lb = QHBoxLayout()
lb.addStretch(1)
self.clear_button = w = QPushButton(_('C&lear'))
lb.addWidget(w)
self.delete_button = w = QPushButton(_('&Delete'))
lb.addWidget(w)
self.replace_button = w = QPushButton(_('&Replace'))
lb.addWidget(w)
self.create_button = w = QPushButton(_('&Create'))
lb.addWidget(w)
lb.addStretch(1)
l.addLayout(lb, 9, 1)
l.addItem(QSpacerItem(10, 10, vPolicy=QSizePolicy.Expanding), 10, 0, -1, -1)
self.setLayout(l)
def initialize(self):
self.funcs = {}
for v in self.db.prefs.get('user_template_functions', []):
if not function_pref_is_python(v):
self.funcs.update({function_pref_name(v):compile_user_function(*v)})
self.build_function_names_box()
self.function_name.currentIndexChanged[native_string_type].connect(self.function_index_changed)
self.function_name.editTextChanged.connect(self.function_name_edited)
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)
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()
self.documentation.clear()
self.create_button.setEnabled(False)
self.delete_button.setEnabled(False)
def build_function_names_box(self, scroll_to=''):
self.function_name.blockSignals(True)
func_names = sorted(self.funcs)
self.function_name.clear()
self.function_name.addItem('')
self.function_name.addItems(func_names)
self.function_name.setCurrentIndex(0)
self.function_name.blockSignals(False)
if scroll_to:
idx = self.function_name.findText(scroll_to)
if idx >= 0:
self.function_name.setCurrentIndex(idx)
def delete_button_clicked(self):
name = unicode_type(self.function_name.currentText())
if name in self.funcs:
del self.funcs[name]
self.changed_signal.emit()
self.create_button.setEnabled(True)
self.delete_button.setEnabled(False)
self.build_function_names_box()
self.program.setReadOnly(False)
else:
error_dialog(self.gui, _('Stored templates'),
_('Function not defined'), show=True)
def create_button_clicked(self, use_name=None):
self.changed_signal.emit()
name = use_name if use_name else unicode_type(self.function_name.currentText())
for k,v in formatter_functions().get_functions().items():
if k == name and v.is_python:
error_dialog(self.gui, _('Stored templates'),
_('Name %s is already used for template function')%(name,), show=True)
try:
prog = unicode_type(self.program.text())
if not prog.startswith('program:'):
error_dialog(self.gui, _('Stored templates'),
_('The stored template must begin with "program:"'), show=True)
cls = compile_user_function(name, unicode_type(self.documentation.toPlainText()),
0, prog)
self.funcs[name] = cls
self.build_function_names_box(scroll_to=name)
except:
error_dialog(self.gui, _('Stored templates'),
_('Exception while storing template'), show=True,
det_msg=traceback.format_exc())
def function_name_edited(self, txt):
self.documentation.setReadOnly(False)
self.create_button.setEnabled(True)
self.replace_button.setEnabled(False)
self.program.setReadOnly(False)
def function_index_changed(self, txt):
txt = unicode_type(txt)
self.create_button.setEnabled(False)
if not txt:
self.program.clear()
self.documentation.clear()
self.documentation.setReadOnly(False)
return
func = self.funcs[txt]
self.documentation.setText(func.doc)
self.program.setText(func.program_text)
self.delete_button.setEnabled(True)
self.program.setReadOnly(False)
self.replace_button.setEnabled(False)
def replace_button_clicked(self):
name = unicode_type(self.function_name.currentText())
self.delete_button_clicked()
self.create_button_clicked(use_name=name)
def refresh_gui(self, gui):
pass
def commit(self):
# formatter_functions().reset_to_builtins()
pref_value = [v for v in self.db.prefs.get('user_template_functions', [])
if function_pref_is_python(v)]
for v in self.funcs.values():
pref_value.append(v.to_pref())
self.db.new_api.set_pref('user_template_functions', pref_value)
funcs = compile_user_template_functions(pref_value)
self.db.new_api.set_user_template_functions(funcs)
self.gui.library_view.model().refresh()
load_user_template_functions(self.db.library_id, [], funcs)
return False
if __name__ == '__main__':
from PyQt5.Qt import QApplication
app = QApplication([])
test_widget('Advanced', 'StoredTemplates')

View File

@ -16,7 +16,8 @@ from calibre.gui2.preferences.template_functions_ui import Ui_Form
from calibre.gui2.widgets import PythonHighlighter from calibre.gui2.widgets import PythonHighlighter
from calibre.utils.formatter_functions import (formatter_functions, from calibre.utils.formatter_functions import (formatter_functions,
compile_user_function, compile_user_template_functions, compile_user_function, compile_user_template_functions,
load_user_template_functions) load_user_template_functions, function_pref_is_python,
function_pref_name)
from polyglot.builtins import iteritems, native_string_type, unicode_type from polyglot.builtins import iteritems, native_string_type, unicode_type
@ -86,7 +87,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
traceback.print_exc() traceback.print_exc()
self.builtin_source_dict = {} self.builtin_source_dict = {}
self.funcs = formatter_functions().get_functions() self.funcs = dict((k,v) for k,v in formatter_functions().get_functions().items()
if v.is_python)
self.builtins = formatter_functions().get_builtins_and_aliases() self.builtins = formatter_functions().get_builtins_and_aliases()
self.build_function_names_box() self.build_function_names_box()
@ -116,16 +119,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.create_button.setEnabled(False) self.create_button.setEnabled(False)
self.delete_button.setEnabled(False) self.delete_button.setEnabled(False)
def build_function_names_box(self, scroll_to='', set_to=''): def build_function_names_box(self, scroll_to=''):
self.function_name.blockSignals(True) self.function_name.blockSignals(True)
func_names = sorted(self.funcs) func_names = sorted(self.funcs)
self.function_name.clear() self.function_name.clear()
self.function_name.addItem('') self.function_name.addItem('')
self.function_name.addItems(func_names) self.function_name.addItems(func_names)
self.function_name.setCurrentIndex(0) self.function_name.setCurrentIndex(0)
if set_to:
self.function_name.setEditText(set_to)
self.create_button.setEnabled(True)
self.function_name.blockSignals(False) self.function_name.blockSignals(False)
if scroll_to: if scroll_to:
idx = self.function_name.findText(scroll_to) idx = self.function_name.findText(scroll_to)
@ -140,23 +140,30 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
error_dialog(self.gui, _('Template functions'), error_dialog(self.gui, _('Template functions'),
_('You cannot delete a built-in function'), show=True) _('You cannot delete a built-in function'), show=True)
if name in self.funcs: if name in self.funcs:
print('delete')
del self.funcs[name] del self.funcs[name]
self.changed_signal.emit() self.changed_signal.emit()
self.create_button.setEnabled(True) self.create_button.setEnabled(True)
self.delete_button.setEnabled(False) self.delete_button.setEnabled(False)
self.build_function_names_box(set_to=name) self.build_function_names_box()
self.program.setReadOnly(False) self.program.setReadOnly(False)
else: else:
error_dialog(self.gui, _('Template functions'), error_dialog(self.gui, _('Template functions'),
_('Function not defined'), show=True) _('Function not defined'), show=True)
def create_button_clicked(self): def create_button_clicked(self, use_name=None):
self.changed_signal.emit() self.changed_signal.emit()
name = unicode_type(self.function_name.currentText()) name = use_name if use_name else unicode_type(self.function_name.currentText())
if name in self.funcs: if name in self.funcs:
error_dialog(self.gui, _('Template functions'), error_dialog(self.gui, _('Template functions'),
_('Name %s already used')%(name,), show=True) _('Name %s already used')%(name,), show=True)
return return
if name in {function_pref_name(v) for v in
self.db.prefs.get('user_template_functions', [])
if not function_pref_is_python(v)}:
error_dialog(self.gui, _('Template functions'),
_('Name %s is already used for stored template')%(name,), show=True)
return
if self.argument_count.value() == 0: if self.argument_count.value() == 0:
box = warning_dialog(self.gui, _('Template functions'), box = warning_dialog(self.gui, _('Template functions'),
_('Argument count should be -1 or greater than zero. ' _('Argument count should be -1 or greater than zero. '
@ -215,18 +222,19 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.replace_button.setEnabled(False) self.replace_button.setEnabled(False)
def replace_button_clicked(self): def replace_button_clicked(self):
name = unicode_type(self.function_name.currentText())
self.delete_button_clicked() self.delete_button_clicked()
self.create_button_clicked() self.create_button_clicked(use_name=name)
def refresh_gui(self, gui): def refresh_gui(self, gui):
pass pass
def commit(self): def commit(self):
# formatter_functions().reset_to_builtins() pref_value = [v for v in self.db.prefs.get('user_template_functions', [])
pref_value = [] if not function_pref_is_python(v)]
for name, cls in iteritems(self.funcs): for name, cls in iteritems(self.funcs):
if name not in self.builtins: if name not in self.builtins:
pref_value.append((cls.name, cls.doc, cls.arg_count, cls.program_text)) pref_value.append(cls.to_pref())
self.db.new_api.set_pref('user_template_functions', pref_value) self.db.new_api.set_pref('user_template_functions', pref_value)
funcs = compile_user_template_functions(pref_value) funcs = compile_user_template_functions(pref_value)
self.db.new_api.set_user_template_functions(funcs) self.db.new_api.set_user_template_functions(funcs)

View File

@ -28,6 +28,8 @@ class Node(object):
NODE_CONSTANT = 7 NODE_CONSTANT = 7
NODE_FIELD = 8 NODE_FIELD = 8
NODE_RAW_FIELD = 9 NODE_RAW_FIELD = 9
NODE_CALL = 10
NODE_ARGUMENTS = 11
class IfNode(Node): class IfNode(Node):
@ -55,6 +57,21 @@ class FunctionNode(Node):
self.expression_list = expression_list self.expression_list = expression_list
class CallNode(Node):
def __init__(self, function, expression_list):
Node.__init__(self)
self.node_type = self.NODE_CALL
self.function = function
self.expression_list = expression_list
class ArgumentsNode(Node):
def __init__(self, expression_list):
Node.__init__(self)
self.node_type = self.NODE_ARGUMENTS
self.expression_list = expression_list
class StringInfixNode(Node): class StringInfixNode(Node):
def __init__(self, operator, left, right): def __init__(self, operator, left, right):
Node.__init__(self) Node.__init__(self)
@ -188,6 +205,13 @@ class _Parser(object):
except: except:
return False return False
def token_is_call(self):
try:
token = self.prog[self.lex_pos]
return token[1] == 'call' and token[0] == self.LEX_KEYWORD
except:
return False
def token_is_if(self): def token_is_if(self):
try: try:
token = self.prog[self.lex_pos] token = self.prog[self.lex_pos]
@ -228,9 +252,11 @@ class _Parser(object):
except: except:
return True return True
def program(self, funcs, prog): def program(self, parent, funcs, prog):
self.lex_pos = 0 self.lex_pos = 0
self.parent = parent
self.funcs = funcs self.funcs = funcs
self.func_names = frozenset(set(self.funcs.keys()) | {'arguments',})
self.prog = prog[0] self.prog = prog[0]
self.prog_len = len(self.prog) self.prog_len = len(self.prog)
if prog[1] != '': if prog[1] != '':
@ -276,9 +302,37 @@ class _Parser(object):
return NumericInfixNode(operator, left, self.expr()) return NumericInfixNode(operator, left, self.expr())
return left return left
def call_expression(self):
self.consume()
if not self.token_is_id():
self.error(_('"call" requires a stored template name'))
name = self.token()
if name not in self.func_names or self.funcs[name].is_python:
self.error(_('{} is not a stored template').format(name))
text = self.funcs[name].program_text
if not text.startswith('program:'):
self.error((_('A stored template must begin with program:')))
text = text[len('program:'):]
if not self.token_op_is_lparen():
self.error(_('"call" requires arguments surrounded by "(" ")"'))
self.consume()
arguments = list()
while not self.token_op_is_rparen():
arguments.append(self.infix_expr())
if not self.token_op_is_comma():
break
self.consume()
if self.token() != ')':
self.error(_('Missing closing parenthesis'))
subprog = _Parser().program(self, self.funcs,
self.parent.lex_scanner.scan(text))
return CallNode(subprog, arguments)
def expr(self): def expr(self):
if self.token_is_if(): if self.token_is_if():
return self.if_expression() return self.if_expression()
if self.token_is_call():
return self.call_expression()
if self.token_is_id(): if self.token_is_id():
# We have an identifier. Determine if it is a function # We have an identifier. Determine if it is a function
id_ = self.token() id_ = self.token()
@ -293,7 +347,7 @@ class _Parser(object):
# Check if it is a known one. We do this here so error reporting is # Check if it is a known one. We do this here so error reporting is
# better, as it can identify the tokens near the problem. # better, as it can identify the tokens near the problem.
id_ = id_.strip() id_ = id_.strip()
if id_ not in self.funcs: if id_ not in self.func_names:
self.error(_('Unknown function {0}').format(id_)) self.error(_('Unknown function {0}').format(id_))
# Eat the paren # Eat the paren
self.consume() self.consume()
@ -314,9 +368,19 @@ class _Parser(object):
return IfNode(arguments[0], (arguments[1],), (arguments[2],)) return IfNode(arguments[0], (arguments[1],), (arguments[2],))
if (id_ == 'assign' and len(arguments) == 2 and arguments[0].node_type == Node.NODE_RVALUE): if (id_ == 'assign' and len(arguments) == 2 and arguments[0].node_type == Node.NODE_RVALUE):
return AssignNode(arguments[0].name, arguments[1]) return AssignNode(arguments[0].name, arguments[1])
if id_ == 'arguments':
new_args = []
for arg in arguments:
if arg.node_type not in (Node.NODE_ASSIGN, Node.NODE_RVALUE):
self.error(_("Parameters to 'arguments' must be "
"variables or assignments"))
if arg.node_type == Node.NODE_RVALUE:
arg = AssignNode(arg.name, ConstantNode(''))
new_args.append(arg)
return ArgumentsNode(new_args)
cls = self.funcs[id_] cls = self.funcs[id_]
if cls.arg_count != -1 and len(arguments) != cls.arg_count: if cls.arg_count != -1 and len(arguments) != cls.arg_count:
self.error(_('Incorrect number of expression_list for function {0}').format(id_)) self.error(_('Incorrect number of arguments for function {0}').format(id_))
return FunctionNode(id_, arguments) return FunctionNode(id_, arguments)
elif self.token_is_constant(): elif self.token_is_constant():
# String or number # String or number
@ -408,6 +472,24 @@ class _Interpreter(object):
return cls.eval_(self.parent, self.parent_kwargs, return cls.eval_(self.parent, self.parent_kwargs,
self.parent_book, self.locals, *args) self.parent_book, self.locals, *args)
def do_node_call(self, prog):
args = list()
for arg in prog.expression_list:
# evaluate the expression (recursive call)
args.append(self.expr(arg))
saved_locals = self.locals
self.locals = {}
for dex, v in enumerate(args):
self.locals['*arg_'+ str(dex)] = v
val = self.expression_list(prog.function)
self.locals = saved_locals
return val
def do_node_arguments(self, prog):
for dex,arg in enumerate(prog.expression_list):
self.locals[arg.left] = self.locals.get('*arg_'+ str(dex), self.expr(arg.right))
return ''
def do_node_constant(self, prog): def do_node_constant(self, prog):
return prog.value return prog.value
@ -454,6 +536,8 @@ class _Interpreter(object):
Node.NODE_RAW_FIELD: do_node_raw_field, Node.NODE_RAW_FIELD: do_node_raw_field,
Node.NODE_STRING_INFIX: do_node_string_infix, Node.NODE_STRING_INFIX: do_node_string_infix,
Node.NODE_NUMERIC_INFIX: do_node_numeric_infix, Node.NODE_NUMERIC_INFIX: do_node_numeric_infix,
Node.NODE_ARGUMENTS: do_node_arguments,
Node.NODE_CALL: do_node_call,
} }
def expr(self, prog): def expr(self, prog):
@ -536,7 +620,7 @@ class TemplateFormatter(string.Formatter):
lex_scanner = re.Scanner([ lex_scanner = re.Scanner([
(r'(==#|!=#|<=#|<#|>=#|>#)', lambda x,t: (_Parser.LEX_NUMERIC_INFIX, t)), (r'(==#|!=#|<=#|<#|>=#|>#)', lambda x,t: (_Parser.LEX_NUMERIC_INFIX, t)),
(r'(==|!=|<=|<|>=|>)', lambda x,t: (_Parser.LEX_STRING_INFIX, t)), # noqa (r'(==|!=|<=|<|>=|>)', lambda x,t: (_Parser.LEX_STRING_INFIX, t)), # noqa
(r'(if|then|else|fi)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa (r'(if|then|else|fi|call)\b',lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa
(r'[(),=;]', lambda x,t: (_Parser.LEX_OP, t)), # noqa (r'[(),=;]', lambda x,t: (_Parser.LEX_OP, t)), # noqa
(r'-?[\d\.]+', lambda x,t: (_Parser.LEX_CONST, t)), # noqa (r'-?[\d\.]+', lambda x,t: (_Parser.LEX_CONST, t)), # noqa
(r'\$', lambda x,t: (_Parser.LEX_ID, t)), # noqa (r'\$', lambda x,t: (_Parser.LEX_ID, t)), # noqa
@ -554,10 +638,10 @@ class TemplateFormatter(string.Formatter):
if column_name is not None and self.template_cache is not None: if column_name is not None and self.template_cache is not None:
tree = self.template_cache.get(column_name, None) tree = self.template_cache.get(column_name, None)
if not tree: if not tree:
tree = self.gpm_parser.program(self.funcs, self.lex_scanner.scan(prog)) tree = self.gpm_parser.program(self, self.funcs, self.lex_scanner.scan(prog))
self.template_cache[column_name] = tree self.template_cache[column_name] = tree
else: else:
tree = self.gpm_parser.program(self.funcs, self.lex_scanner.scan(prog)) tree = self.gpm_parser.program(self, self.funcs, self.lex_scanner.scan(prog))
return self.gpm_interpreter.program(self.funcs, self, tree, val) return self.gpm_interpreter.program(self.funcs, self, tree, val)
# ################# Override parent classes methods ##################### # ################# Override parent classes methods #####################

View File

@ -126,6 +126,7 @@ class FormatterFunction(object):
category = 'Unknown' category = 'Unknown'
arg_count = 0 arg_count = 0
aliases = [] aliases = []
is_python = True
def evaluate(self, formatter, kwargs, mi, locals, *args): def evaluate(self, formatter, kwargs, mi, locals, *args):
raise NotImplementedError() raise NotImplementedError()
@ -1813,17 +1814,35 @@ _formatter_builtins = [
class FormatterUserFunction(FormatterFunction): class FormatterUserFunction(FormatterFunction):
def __init__(self, name, doc, arg_count, program_text): def __init__(self, name, doc, arg_count, program_text, is_python):
self.is_python = is_python
self.name = name self.name = name
self.doc = doc self.doc = doc
self.arg_count = arg_count self.arg_count = arg_count
self.program_text = program_text self.program_text = program_text
def to_pref(self):
return [self.name, self.doc, self.arg_count, self.program_text]
tabs = re.compile(r'^\t*') tabs = re.compile(r'^\t*')
def function_pref_is_python(pref):
if isinstance(pref, list):
pref = pref[3]
if pref.startswith('def'):
return True
if pref.startswith('program'):
return False
raise ValueError('Unknown program type in formatter function pref')
def function_pref_name(pref):
return pref[0]
def compile_user_function(name, doc, arg_count, eval_func): def compile_user_function(name, doc, arg_count, eval_func):
if not function_pref_is_python(eval_func):
return FormatterUserFunction(name, doc, arg_count, eval_func, False)
def replace_func(mo): def replace_func(mo):
return mo.group().replace('\t', ' ') return mo.group().replace('\t', ' ')
@ -1838,7 +1857,7 @@ class UserFunction(FormatterUserFunction):
if DEBUG and tweaks.get('enable_template_debug_printing', False): if DEBUG and tweaks.get('enable_template_debug_printing', False):
print(prog) print(prog)
exec(prog, locals_) exec(prog, locals_)
cls = locals_['UserFunction'](name, doc, arg_count, eval_func) cls = locals_['UserFunction'](name, doc, arg_count, eval_func, True)
return cls return cls
@ -1855,6 +1874,7 @@ def compile_user_template_functions(funcs):
# then white space differences don't cause them to compare differently # then white space differences don't cause them to compare differently
cls = compile_user_function(*func) cls = compile_user_function(*func)
cls.is_python = function_pref_is_python(func)
compiled_funcs[cls.name] = cls compiled_funcs[cls.name] = cls
except Exception: except Exception:
try: try:
@ -1862,7 +1882,7 @@ def compile_user_template_functions(funcs):
except Exception: except Exception:
func_name = 'Unknown' func_name = 'Unknown'
prints('**** Compilation errors in user template function "%s" ****' % func_name) prints('**** Compilation errors in user template function "%s" ****' % func_name)
traceback.print_exc(limit=0) traceback.print_exc(limit=10)
prints('**** End compilation errors in %s "****"' % func_name) prints('**** End compilation errors in %s "****"' % func_name)
return compiled_funcs return compiled_funcs