From dd8d3bfeef2bd06353a18a505dcf77ab36d200a7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 17 Oct 2022 20:11:16 +0530 Subject: [PATCH] Cleanup previous PR --- manual/template_lang.rst | 4 +-- src/calibre/utils/formatter.py | 59 ++++++++++++++++------------------ 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/manual/template_lang.rst b/manual/template_lang.rst index 602a9306c9..fb91be40db 100644 --- a/manual/template_lang.rst +++ b/manual/template_lang.rst @@ -661,7 +661,7 @@ A PTM template begins with: # db: a calibre legacy database object # globals: the template global variable dictionary # arguments: is a list of arguments if the template is called by a GPM template, otherwise None - # funcs: allows to use the Builtin/User functions and Stored GPM/Python templates + # funcs: allows using the Builtin/User functions and Stored GPM/Python templates # your Python code goes here return 'a string' @@ -670,7 +670,7 @@ You can add the above text to your template using the context menu, usually acce The context object supports ``str(context)`` that returns a string of the context's contents, and ``context.attributes`` that returns a list of the attribute names in the context. -The ``context.funcs`` attribute allows to use the Builtin and User functions, and also the Stored GPM/Python templates so that you can exectute them directly in your code. The functions can be retrieve by they name and they name plus a '_' at the end in case of conflict with Python language keywords. +The ``context.funcs`` attribute allows using the Builtin and User functions, and also the Stored GPM/Python templates, so that you can execute them directly in your code. The functions can be retrieve by their names. If the name conflicts with a Python keyword, add an underscore to the end of the name. Here is an example of a PTM template that produces a list of all the authors for a series. The list is stored in a `Column built from other columns, behaves like tags`. It shows in :guilabel:`Book details` and has the :guilabel:`on separate lines` checked (in :guilabel:`Preferences->Look & feel->Book details`). That option requires the list to be comma-separated. To satisfy that requirement the template converts commas in author names to semicolons then builds a comma-separated list of authors. The authors are then sorted, which is why the template uses author_sort. diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 3b016641ad..5a0042a02c 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -866,60 +866,57 @@ class PythonTemplateContext(object): return '\n'.join(f'{k}:{v}' for k,v in ans.items()) -class FormatterFuncsCaller(): +class FormatterFuncsCaller: ''' - Provides a convenient solution for call the funcs loaded in a TemplateFormatter - The funcs can be called by their name as attribut of this class, plus a _ 'underscore' a the end (Python keyword conflicts) - If the name contain a illegal character for a attribut (like .:-), use getattr() + Provides a convenient solution to call the functions loaded in a TemplateFormatter. + The funcs can be called by their name as attributes of this class, with a underscore at the end if the name conflicts with a Python keyword. + If the name contain a illegal character for a attribute (like .:-), use getattr() ''' def __init__(self, formatter): - object.__init__(self) if not isinstance(formatter, TemplateFormatter): - raise ValueError('Class {} is not an instance of TemplateFormatter' - .format(formatter.__class__.__name__)) + raise TypeError(f'{formatter} is not an instance of TemplateFormatter') self.__formatter__ = formatter def __getattribute__(self, name): - if name.startswith('__') and name.endswith('__'): # return internal special attribut + if name.startswith('__') and name.endswith('__'): # return internal special attribute try: return object.__getattribute__(self, name) - except: + except Exception: pass formatter = self.__formatter__ - func_name = None - if name.endswith('_') and name[:-1] in formatter.funcs: #given the priority to the backup name + func_name = '' + if name.endswith('_') and name[:-1] in formatter.funcs: # give the priority to the backup name func_name = name[:-1] elif name in formatter.funcs: func_name = name if func_name: + def call(*args, **kargs): def n(d): - return str('' if d is None else d) - args = [n(a) for a in args] + return '' if d is None else str(d) + args = tuple(n(a) for a in args) try: - def raise_error(msg): - raise ValueError(msg) if kargs: - raise_error(_('Got an unsupported keyword argument')) - + raise ValueError(_('Keyword arguments are not allowed')) + # special function if func_name == 'arguments': - raise_error(_('Get the arguments from context.arguments instead of calling arguments()')) - elif func_name == 'globals': - raise_error(_('Get the globals from context.globals instead of calling globals()')) - elif func_name == 'set_globals': - raise_error(_("Set globals using context.globals['name'] = val instead of calling set_globals()")) - elif func_name == 'character': + raise ValueError(_('Get the arguments from context.arguments instead of calling arguments()')) + if func_name == 'globals': + raise ValueError(_('Get the globals from context.globals instead of calling globals()')) + if func_name == 'set_globals': + raise ValueError(_("Set globals using context.globals['name'] = val instead of calling set_globals()")) + if func_name == 'character': if _Parser.inlined_function_nodes['character'][0](args): - rslt = _Interpreter.characters.get(args[0], None) + rslt = _Interpreter.characters.get(args[0]) if rslt is None: - raise_error(_("Invalid character name '{0}'").format(args[0])) + raise ValueError(_("Invalid character name '{0}'").format(args[0])) else: - raise_error(_('Incorrect number of arguments')) + raise ValueError(_('Incorrect number of arguments')) else: # builtin/user function and Stored GPM/Python template func = formatter.funcs[func_name] @@ -927,19 +924,19 @@ class FormatterFuncsCaller(): rslt = func.evaluate(formatter, formatter.kwargs, formatter.book, formatter.locals, *args) else: rslt = formatter._eval_sfm_call(func_name, args, formatter.global_vars) - + except Exception as e: # Change the error message to return this used name on the template e = e.__class__(_('Error in the function {0} :: {1}').format( name, - re.sub(r'\w+\.evaluate\(\)\s*', '', str(e), 1))) # remove UserFunction.evaluate() | Builtin*.evaluate() + re.sub(r'\w+\.evaluate\(\)\s*', '', str(e), 1))) # remove UserFunction.evaluate() | Builtin*.evaluate() e.is_internal = True raise e return rslt - + return call - e = AttributeError(_("no function '{}' exists").format(name)) + e = AttributeError(_("no function named {!r} exists").format(name)) e.is_internal = True raise e @@ -1701,7 +1698,7 @@ class TemplateFormatter(string.Formatter): if s.filename == '': ss = s break - + raise ValueError(_('Error in function {0} on line {1} : {2} - {3}').format( ss.name, ss.lineno, type(e).__name__, str(e))) if not isinstance(rslt, str):