diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 70d0bb106d..7c10ede34d 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -935,6 +935,12 @@ class ActionTemplateTester(InterfaceActionBase): 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): name = 'Save To Disk' actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction' @@ -1099,7 +1105,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionCopyToLibrary, ActionTweakEpub, ActionUnpackBook, ActionNextMatch, ActionStore, ActionPluginUpdater, ActionPickRandom, ActionEditToC, ActionSortBy, ActionMarkBooks, ActionEmbed, ActionTemplateTester, ActionTagMapper, ActionAuthorMapper, - ActionVirtualLibrary, ActionBrowseAnnotations] + ActionVirtualLibrary, ActionBrowseAnnotations, ActionStoredTemplates] # }}} @@ -1272,6 +1278,18 @@ class TemplateFunctions(PreferencesPlugin): 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): name = 'Email' icon = I('mail.png') @@ -1376,7 +1394,7 @@ class Misc(PreferencesPlugin): plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions, CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard, Email, Server, Plugins, Tweaks, Misc, TemplateFunctions, - MetadataSources, Keyboard, IgnoredDevices] + StoredTemplates, MetadataSources, Keyboard, IgnoredDevices] # }}} diff --git a/src/calibre/gui2/actions/show_stored_templates.py b/src/calibre/gui2/actions/show_stored_templates.py new file mode 100644 index 0000000000..9164a7d5ba --- /dev/null +++ b/src/calibre/gui2/actions/show_stored_templates.py @@ -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 ' +__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_() + diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index 19d720eb05..81706fbb40 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -429,12 +429,17 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): name = unicode_type(toWhat) self.source_code.clear() self.documentation.clear() + self.func_type.clear() if name in self.funcs: self.documentation.setPlainText(self.funcs[name].doc) if name in self.builtins and name in self.builtin_source_dict: self.source_code.setPlainText(self.builtin_source_dict[name]) else: 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): txt = unicode_type(self.textbox.toPlainText()).rstrip() diff --git a/src/calibre/gui2/dialogs/template_dialog.ui b/src/calibre/gui2/dialogs/template_dialog.ui index 2b3657fb4c..9f6d38399c 100644 --- a/src/calibre/gui2/dialogs/template_dialog.ui +++ b/src/calibre/gui2/dialogs/template_dialog.ui @@ -224,6 +224,26 @@ + + + &Function type: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + func_type + + + + + + + true + + + + &Documentation: @@ -236,10 +256,10 @@ - + - Python &code: + &Code: Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop @@ -249,7 +269,7 @@ - + @@ -259,17 +279,17 @@ - + - + true - + true diff --git a/src/calibre/gui2/preferences/stored_templates.py b/src/calibre/gui2/preferences/stored_templates.py new file mode 100644 index 0000000000..d41c35ee41 --- /dev/null +++ b/src/calibre/gui2/preferences/stored_templates.py @@ -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 ' +__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 = '

' + _(''' + 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. + ''') + '

' + 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') diff --git a/src/calibre/gui2/preferences/template_functions.py b/src/calibre/gui2/preferences/template_functions.py index f1ade855af..269a3a3c80 100644 --- a/src/calibre/gui2/preferences/template_functions.py +++ b/src/calibre/gui2/preferences/template_functions.py @@ -16,7 +16,8 @@ from calibre.gui2.preferences.template_functions_ui import Ui_Form from calibre.gui2.widgets import PythonHighlighter from calibre.utils.formatter_functions import (formatter_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 @@ -86,7 +87,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): traceback.print_exc() 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.build_function_names_box() @@ -116,16 +119,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.create_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) func_names = sorted(self.funcs) self.function_name.clear() self.function_name.addItem('') self.function_name.addItems(func_names) self.function_name.setCurrentIndex(0) - if set_to: - self.function_name.setEditText(set_to) - self.create_button.setEnabled(True) self.function_name.blockSignals(False) if scroll_to: idx = self.function_name.findText(scroll_to) @@ -140,23 +140,30 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): error_dialog(self.gui, _('Template functions'), _('You cannot delete a built-in function'), show=True) if name in self.funcs: + print('delete') del self.funcs[name] self.changed_signal.emit() self.create_button.setEnabled(True) self.delete_button.setEnabled(False) - self.build_function_names_box(set_to=name) + self.build_function_names_box() self.program.setReadOnly(False) else: error_dialog(self.gui, _('Template functions'), _('Function not defined'), show=True) - def create_button_clicked(self): + def create_button_clicked(self, use_name=None): 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: error_dialog(self.gui, _('Template functions'), _('Name %s already used')%(name,), show=True) 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: box = warning_dialog(self.gui, _('Template functions'), _('Argument count should be -1 or greater than zero. ' @@ -215,18 +222,19 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): 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() + self.create_button_clicked(use_name=name) def refresh_gui(self, gui): pass def commit(self): - # formatter_functions().reset_to_builtins() - pref_value = [] + pref_value = [v for v in self.db.prefs.get('user_template_functions', []) + if not function_pref_is_python(v)] for name, cls in iteritems(self.funcs): 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) funcs = compile_user_template_functions(pref_value) self.db.new_api.set_user_template_functions(funcs) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 45b85bb280..b8cfaf10e4 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -28,6 +28,8 @@ class Node(object): NODE_CONSTANT = 7 NODE_FIELD = 8 NODE_RAW_FIELD = 9 + NODE_CALL = 10 + NODE_ARGUMENTS = 11 class IfNode(Node): @@ -55,6 +57,21 @@ class FunctionNode(Node): 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): def __init__(self, operator, left, right): Node.__init__(self) @@ -188,6 +205,13 @@ class _Parser(object): except: 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): try: token = self.prog[self.lex_pos] @@ -228,9 +252,11 @@ class _Parser(object): except: return True - def program(self, funcs, prog): + def program(self, parent, funcs, prog): self.lex_pos = 0 + self.parent = parent self.funcs = funcs + self.func_names = frozenset(set(self.funcs.keys()) | {'arguments',}) self.prog = prog[0] self.prog_len = len(self.prog) if prog[1] != '': @@ -276,9 +302,37 @@ class _Parser(object): return NumericInfixNode(operator, left, self.expr()) 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): if self.token_is_if(): return self.if_expression() + if self.token_is_call(): + return self.call_expression() if self.token_is_id(): # We have an identifier. Determine if it is a function 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 # better, as it can identify the tokens near the problem. id_ = id_.strip() - if id_ not in self.funcs: + if id_ not in self.func_names: self.error(_('Unknown function {0}').format(id_)) # Eat the paren self.consume() @@ -314,9 +368,19 @@ class _Parser(object): return IfNode(arguments[0], (arguments[1],), (arguments[2],)) if (id_ == 'assign' and len(arguments) == 2 and arguments[0].node_type == Node.NODE_RVALUE): 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_] 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) elif self.token_is_constant(): # String or number @@ -408,6 +472,24 @@ class _Interpreter(object): return cls.eval_(self.parent, self.parent_kwargs, 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): return prog.value @@ -454,6 +536,8 @@ class _Interpreter(object): Node.NODE_RAW_FIELD: do_node_raw_field, Node.NODE_STRING_INFIX: do_node_string_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): @@ -536,7 +620,7 @@ class TemplateFormatter(string.Formatter): lex_scanner = re.Scanner([ (r'(==#|!=#|<=#|<#|>=#|>#)', lambda x,t: (_Parser.LEX_NUMERIC_INFIX, t)), (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'-?[\d\.]+', lambda x,t: (_Parser.LEX_CONST, 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: tree = self.template_cache.get(column_name, None) 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 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) # ################# Override parent classes methods ##################### diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 3628e1938e..392e52328f 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -126,6 +126,7 @@ class FormatterFunction(object): category = 'Unknown' arg_count = 0 aliases = [] + is_python = True def evaluate(self, formatter, kwargs, mi, locals, *args): raise NotImplementedError() @@ -1813,17 +1814,35 @@ _formatter_builtins = [ 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.doc = doc self.arg_count = arg_count 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*') +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): + if not function_pref_is_python(eval_func): + return FormatterUserFunction(name, doc, arg_count, eval_func, False) + def replace_func(mo): return mo.group().replace('\t', ' ') @@ -1838,7 +1857,7 @@ class UserFunction(FormatterUserFunction): if DEBUG and tweaks.get('enable_template_debug_printing', False): print(prog) exec(prog, locals_) - cls = locals_['UserFunction'](name, doc, arg_count, eval_func) + cls = locals_['UserFunction'](name, doc, arg_count, eval_func, True) return cls @@ -1855,6 +1874,7 @@ def compile_user_template_functions(funcs): # then white space differences don't cause them to compare differently cls = compile_user_function(*func) + cls.is_python = function_pref_is_python(func) compiled_funcs[cls.name] = cls except Exception: try: @@ -1862,7 +1882,7 @@ def compile_user_template_functions(funcs): except Exception: func_name = 'Unknown' 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) return compiled_funcs