diff --git a/src/calibre/ebooks/metadata/worker.py b/src/calibre/ebooks/metadata/worker.py index 660240571b..e8147958d8 100644 --- a/src/calibre/ebooks/metadata/worker.py +++ b/src/calibre/ebooks/metadata/worker.py @@ -318,7 +318,7 @@ def save_book(ids, dpath, plugboards, template_functions, path, recs, from calibre.library.save_to_disk import config, save_serialized_to_disk from calibre.customize.ui import apply_null_metadata from calibre.utils.formatter_functions import load_user_template_functions - load_user_template_functions(template_functions) + load_user_template_functions('', template_functions) opts = config().parse() for name in recs: setattr(opts, name, recs[name]) diff --git a/src/calibre/gui2/preferences/template_functions.py b/src/calibre/gui2/preferences/template_functions.py index f5203e9cdd..4cc07fee3a 100644 --- a/src/calibre/gui2/preferences/template_functions.py +++ b/src/calibre/gui2/preferences/template_functions.py @@ -223,7 +223,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): if f in self.builtins: continue func = self.funcs[f] - formatter_functions().register_function(func) + formatter_functions().register_function(self.db.library_id, func) pref_value.append((func.name, func.doc, func.arg_count, func.program_text)) self.db.prefs.set('user_template_functions', pref_value) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index b4d103ca64..80aa66601b 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -570,6 +570,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ olddb = self.library_view.model().db if copy_structure: default_prefs = olddb.prefs + + from calibre.utils.formatter_functions import unload_user_template_functions + unload_user_template_functions(olddb.library_id ) except: olddb = None try: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 677729a9e9..749be044d2 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -325,7 +325,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.prefs.set('user_categories', user_cats) if not self.is_second_db: - load_user_template_functions(self.prefs.get('user_template_functions', [])) + load_user_template_functions(self.library_id, + self.prefs.get('user_template_functions', [])) # Load the format filename cache self.refresh_format_cache() diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index eff38203a0..2fa5901dde 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -137,6 +137,7 @@ class _Parser(object): # We have a function. # 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 funcs: self.error(_('unknown function {0}').format(id)) @@ -246,6 +247,7 @@ class _CompileParser(_Parser): # We have a function. # 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 funcs: self.error(_('unknown function {0}').format(id)) @@ -457,7 +459,7 @@ class TemplateFormatter(string.Formatter): colon += 1 funcs = formatter_functions().get_functions() - fname = fmt[colon:p] + fname = fmt[colon:p].strip() if fname in funcs: func = funcs[fname] if func.arg_count == 2: diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 73dad7422b..f8d62c367a 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -10,6 +10,7 @@ __docformat__ = 'restructuredtext en' import inspect, re, traceback from math import trunc +from collections import defaultdict from calibre import human_readable from calibre.constants import DEBUG @@ -25,6 +26,7 @@ class FormatterFunctions(object): def __init__(self): self._builtins = {} self._functions = {} + self._functions_from_library = defaultdict(list) def register_builtin(self, func_class): if not isinstance(func_class, FormatterFunction): @@ -38,14 +40,24 @@ class FormatterFunctions(object): for a in func_class.aliases: self._functions[a] = func_class - def register_function(self, func_class): + def register_function(self, library_uuid, func_class, replace=False): if not isinstance(func_class, FormatterFunction): raise ValueError('Class %s is not an instance of FormatterFunction'%( func_class.__class__.__name__)) name = func_class.name - if name in self._functions: + if not replace and name in self._functions: raise ValueError('Name %s already used'%name) self._functions[name] = func_class + self._functions_from_library[library_uuid].append(name) + + def function_exists(self, name): + return self._functions.get(name, None) + + def unregister_functions(self, library_uuid): + if library_uuid in self._functions_from_library: + for name in self._functions_from_library[library_uuid]: + self._functions.pop(name) + self._functions_from_library.pop(library_uuid) def get_builtins(self): return self._builtins @@ -1259,11 +1271,45 @@ class UserFunction(FormatterUserFunction): cls = locals_['UserFunction'](name, doc, arg_count, eval_func) return cls -def load_user_template_functions(funcs): - formatter_functions().reset_to_builtins() +error_function_body = ('def evaluate(self, formatter, kwargs, mi, locals):\n' + '\treturn "' + + _('Duplicate user function name {0}. ' + 'Change the name or ensure that the functions are identical') + + '"') + +def load_user_template_functions(library_uuid, funcs): + unload_user_template_functions(library_uuid) + for func in funcs: try: + # Force a name conflict to test the logic + # if func[0] == 'myFunc2': + # func[0] = 'myFunc3' + + # Compile the function so that the tab processing is done on the + # source. This helps ensure that if the function already is defined + # then white space differences don't cause them to compare differently + cls = compile_user_function(*func) - formatter_functions().register_function(cls) + f = formatter_functions().function_exists(cls.name) + replace = False + if f is not None: + existing_body = f.program_text + new_body = cls.program_text + if new_body != existing_body: + # Change the body of the template function to one that will + # return an error message. Also change the arg count to + # -1 (variable) to avoid template compilation errors + replace = True + func[3] = error_function_body.format(func[0]) + func[2] = -1 + cls = compile_user_function(*func) + else: + continue + + formatter_functions().register_function(library_uuid, cls, replace=replace) except: traceback.print_exc() + +def unload_user_template_functions(library_uuid): + formatter_functions().unregister_functions(library_uuid) \ No newline at end of file