From 8b3aeb43bbd527712361ba1721bbbfc7c3eef54e Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Thu, 13 Oct 2022 18:14:28 +0200 Subject: [PATCH 01/12] add python template introspection --- src/calibre/utils/formatter.py | 54 ++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 4aba476da9..1e8925c45d 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -838,7 +838,9 @@ class PythonTemplateContext(object): self.db = None self.arguments = None self.globals = None - self.attrs_set = {'db', 'arguments', 'globals'} + self.formatter = None + self.funcs = None + self.attrs_set = {'db', 'arguments', 'globals', 'formatter', 'funcs'} def set_values(self, **kwargs): # Create/set attributes from the named parameters. Doing it this way we @@ -864,6 +866,40 @@ class PythonTemplateContext(object): return '\n'.join(f'{k}:{v}' for k,v in ans.items()) +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() + ''' + + def __init__(self, formatter): + from functools import partial + object.__init__(self) + + def call(name, *args): + func = formatter.funcs[name] + args = [str(a) for a in args] + try: + if func.object_type == StoredObjectType.PythonFunction: + rslt = func.evaluate(formatter, formatter.kwargs, formatter.book, formatter.locals, *args) + else: + rslt = formatter._eval_sfm_call(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\(\)', name+'_()', str(e), 1))) # replace UserFunction.evaluate() | Builtin*.evaluate() by the func name + e.is_internal = True + raise e + + return rslt + + for name in formatter.funcs.keys(): + setattr(self, name+'_', partial(call, name)) # _ at the end to avoid conflicts with the Python keyword + + class _Interpreter: def error(self, message, line_number): m = _('Interpreter: {0} - line number {1}').format(message, line_number) @@ -1601,10 +1637,20 @@ class TemplateFormatter(string.Formatter): self.python_context_object.set_values( db=get_database(self.book, get_database(self.book, None)), globals=self.global_vars, - arguments=arguments) + arguments=arguments, + formatter=self, + funcs=self._caller) rslt = compiled_template(self.book, self.python_context_object) except Exception as e: - ss = traceback.extract_tb(exc_info()[2])[-1] + stack = traceback.extract_tb(exc_info()[2]) + ss = stack[-1] + if getattr(e, 'is_internal', False): + # Exception raised by FormatterFuncsCaller + # get the line inside the current template instead of the FormatterFuncsCaller + for ss in reversed(traceback.extract_tb(exc_info()[2])): + if ss.filename == '': + 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): @@ -1794,6 +1840,7 @@ class TemplateFormatter(string.Formatter): self.python_context_object = python_context_object else: self.python_context_object = PythonTemplateContext() + self._caller = FormatterFuncsCaller(self) return self.evaluate(fmt, [], kwargs, self.global_vars) finally: self.restore_state(state) @@ -1826,6 +1873,7 @@ class TemplateFormatter(string.Formatter): else: self.funcs = formatter_functions().get_functions() self.locals = {} + self._caller = FormatterFuncsCaller(self) try: ans = self.evaluate(fmt, [], kwargs, self.global_vars, break_reporter=break_reporter) except StopException as e: From 3c20c32676bffbfb62030e7fbff04beb8bad5fa9 Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Thu, 13 Oct 2022 19:53:48 +0200 Subject: [PATCH 02/12] fix FrameSummary replacement --- src/calibre/utils/formatter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 1e8925c45d..13e678b83b 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -1647,8 +1647,9 @@ class TemplateFormatter(string.Formatter): if getattr(e, 'is_internal', False): # Exception raised by FormatterFuncsCaller # get the line inside the current template instead of the FormatterFuncsCaller - for ss in reversed(traceback.extract_tb(exc_info()[2])): - if ss.filename == '': + for s in reversed(traceback.extract_tb(exc_info()[2])): + if s.filename == '': + ss = s break raise ValueError(_('Error in function {0} on line {1} : {2} - {3}').format( From e41aed9a2418cbd2e2ad9a231e94c34e7f6b5970 Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Fri, 14 Oct 2022 09:29:36 +0200 Subject: [PATCH 03/12] use __getattribute__ (now dynamic) --- src/calibre/utils/formatter.py | 54 ++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 13e678b83b..741d908173 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -874,30 +874,46 @@ class FormatterFuncsCaller(): ''' def __init__(self, formatter): - from functools import partial object.__init__(self) + if not isinstance(formatter, TemplateFormatter): + raise ValueError('Class {} is not an instance of TemplateFormatter' + .format(formatter.__class__.__name__)) + object.__setattr__(self, 'formatter', formatter) - def call(name, *args): + def __getattribute__(self, name): + if name.endswith('_'): # _ at the end to avoid conflicts with the Python keyword + func_name = name[:-1] + if func_name in self.formatter.funcs: + def call(*args): + return self.call(func_name, *args) + return call + + try: + return object.__getattribute__(self, name) + except Exception as e: + e.is_internal = True + raise e + + def call(self, name, *args): + formatter = self.formatter + args = [str(a) for a in args] + + try: func = formatter.funcs[name] - args = [str(a) for a in args] - try: - if func.object_type == StoredObjectType.PythonFunction: - rslt = func.evaluate(formatter, formatter.kwargs, formatter.book, formatter.locals, *args) - else: - rslt = formatter._eval_sfm_call(name, args, formatter.global_vars) + if func.object_type == StoredObjectType.PythonFunction: + rslt = func.evaluate(formatter, formatter.kwargs, formatter.book, formatter.locals, *args) + else: + rslt = formatter._eval_sfm_call(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\(\)', name+'_()', str(e), 1))) # replace UserFunction.evaluate() | Builtin*.evaluate() by the func name - e.is_internal = True - raise e - - return rslt + 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\(\)', name+'_()', str(e), 1))) # replace UserFunction.evaluate() | Builtin*.evaluate() by the func name + e.is_internal = True + raise e + return rslt - for name in formatter.funcs.keys(): - setattr(self, name+'_', partial(call, name)) # _ at the end to avoid conflicts with the Python keyword class _Interpreter: From 635acc9312718e76f6c3a5ddae12e17cd05c0cfc Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Fri, 14 Oct 2022 10:30:43 +0200 Subject: [PATCH 04/12] sef caller in __init__ --- src/calibre/utils/formatter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 741d908173..4c64784516 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -1542,6 +1542,7 @@ class TemplateFormatter(string.Formatter): self.funcs = formatter_functions().get_functions() self._interpreters = [] self._template_parser = None + self._caller = FormatterFuncsCaller(self) self.recursion_stack = [] self.recursion_level = -1 @@ -1857,7 +1858,6 @@ class TemplateFormatter(string.Formatter): self.python_context_object = python_context_object else: self.python_context_object = PythonTemplateContext() - self._caller = FormatterFuncsCaller(self) return self.evaluate(fmt, [], kwargs, self.global_vars) finally: self.restore_state(state) @@ -1890,7 +1890,6 @@ class TemplateFormatter(string.Formatter): else: self.funcs = formatter_functions().get_functions() self.locals = {} - self._caller = FormatterFuncsCaller(self) try: ans = self.evaluate(fmt, [], kwargs, self.global_vars, break_reporter=break_reporter) except StopException as e: From 3272643eca31509ab73d335cceac8bb698e4327f Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Fri, 14 Oct 2022 10:40:23 +0200 Subject: [PATCH 05/12] add funcs name/attribut to __dir__ --- src/calibre/utils/formatter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 4c64784516..38a6c407bc 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -894,6 +894,9 @@ class FormatterFuncsCaller(): e.is_internal = True raise e + def __dir__(self): + return list(set(object.__dir__(self) + [f+'_' for f in self.formatter.funcs.keys()])) + def call(self, name, *args): formatter = self.formatter args = [str(a) for a in args] From 281850c32cb6f840f69f28b6a4cfc350b07607e3 Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Fri, 14 Oct 2022 13:59:00 +0200 Subject: [PATCH 06/12] special function, move all inside __getattrib__ --- src/calibre/utils/formatter.py | 119 +++++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 34 deletions(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 38a6c407bc..48b56f2767 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -878,45 +878,96 @@ class FormatterFuncsCaller(): if not isinstance(formatter, TemplateFormatter): raise ValueError('Class {} is not an instance of TemplateFormatter' .format(formatter.__class__.__name__)) - object.__setattr__(self, 'formatter', formatter) + self.__formatter__ = formatter def __getattribute__(self, name): - if name.endswith('_'): # _ at the end to avoid conflicts with the Python keyword - func_name = name[:-1] - if func_name in self.formatter.funcs: - def call(*args): - return self.call(func_name, *args) - return call + if name.startswith('__') and name.endswith('__'): # return internal special attribut + try: + return object.__getattribute__(self, name) + except: + pass - try: - return object.__getattribute__(self, name) - except Exception as e: - e.is_internal = True - raise e + formatter = self.__formatter__ + func_name = None + undersore = None + if name.endswith('_') and name[:-1] in formatter.funcs: + func_name = name[:-1] + undersore = True + if not func_name and name in formatter.funcs: + func_name = name + undersore = False + + if func_name: + def call(*args, **kargs): + args = [str(a) for a in args] + kargs = {k:str(v) for k,v in kargs.items()} + + def u(): + return '_' if undersore else '' + def raise_error(msg): + raise ValueError(msg) + def raise_mixed(args, kargs): + if args and kargs: + raise_error(_('You cannot mix keyword arguments and positional arguments in function {0}').format(func_name+u()+'()')) + + try: + # special function + if func_name == 'set_globals': + raise_mixed(args, kargs) + kargs.update({a:'' for a in args}) + formatter.global_vars.update(kargs) + rslt = kargs + + elif func_name == 'globals': + raise_mixed(args, kargs) + kargs.update({a:'' for a in args}) + rslt = {k:formatter.global_vars.get(k, d) for k,d in kargs.items()} + + elif func_name == 'arguments': + raise_mixed(args, kargs) + kargs.update({a:'' for a in args}) + args = formatter.python_context_object.arguments if formatter.python_context_object.arguments else [] + for i,k in enumerate(kargs.keys()): + if i == len(args): break + kargs[k] = str(args[i]) + rslt = kargs + + else: + if kargs: + raise_error(_('You cannot use keyword arguments in function {0}').format(func_name+u()+'()')) + + if func_name == 'character': + if _Parser.inlined_function_nodes['character'][0](args): + rslt = _Interpreter.characters.get(args[0], None) + if rslt is None: + raise_error(_("Invalid character name '{0}'").format(args[0])) + else: + raise_error(_('Incorrect number of arguments for function {0}').format(func_name+u()+'()')) + + # buildin/user function and Stored GPM/Python template + func = formatter.funcs[func_name] + if func.object_type == StoredObjectType.PythonFunction: + 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( + func_name+u(), + re.sub(r'\w+\.evaluate\(\)', func_name+u()+'()', str(e), 1))) # replace UserFunction.evaluate() | Builtin*.evaluate() by the func name + e.is_internal = True + raise e + return rslt + + return call + + e = AttributeError(_("no function '{}' exists").format(name)) + e.is_internal = True + raise e def __dir__(self): - return list(set(object.__dir__(self) + [f+'_' for f in self.formatter.funcs.keys()])) - - def call(self, name, *args): - formatter = self.formatter - args = [str(a) for a in args] - - try: - func = formatter.funcs[name] - if func.object_type == StoredObjectType.PythonFunction: - rslt = func.evaluate(formatter, formatter.kwargs, formatter.book, formatter.locals, *args) - else: - rslt = formatter._eval_sfm_call(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\(\)', name+'_()', str(e), 1))) # replace UserFunction.evaluate() | Builtin*.evaluate() by the func name - e.is_internal = True - raise e - return rslt - + return list(set(object.__dir__(self) + list(self.__formatter__.funcs.keys()) + [f+'_' for f in self.__formatter__.funcs.keys()])) class _Interpreter: From 13b744328b31b4000209d43862d8988975a9e15a Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Fri, 14 Oct 2022 19:35:12 +0200 Subject: [PATCH 07/12] Reduction of the verbosity of error messages --- src/calibre/utils/formatter.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 48b56f2767..2ab2a9dd1d 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -840,7 +840,7 @@ class PythonTemplateContext(object): self.globals = None self.formatter = None self.funcs = None - self.attrs_set = {'db', 'arguments', 'globals', 'formatter', 'funcs'} + self.attrs_set = {'db', 'arguments', 'globals', 'funcs'} def set_values(self, **kwargs): # Create/set attributes from the named parameters. Doing it this way we @@ -881,7 +881,7 @@ class FormatterFuncsCaller(): 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 attribut try: return object.__getattribute__(self, name) except: @@ -890,7 +890,7 @@ class FormatterFuncsCaller(): formatter = self.__formatter__ func_name = None undersore = None - if name.endswith('_') and name[:-1] in formatter.funcs: + if name.endswith('_') and name[:-1] in formatter.funcs: #given the priority to the backup name func_name = name[:-1] undersore = True if not func_name and name in formatter.funcs: @@ -908,7 +908,7 @@ class FormatterFuncsCaller(): raise ValueError(msg) def raise_mixed(args, kargs): if args and kargs: - raise_error(_('You cannot mix keyword arguments and positional arguments in function {0}').format(func_name+u()+'()')) + raise_error(_('Invalid mixing keyword arguments and positional arguments')) try: # special function @@ -934,7 +934,7 @@ class FormatterFuncsCaller(): else: if kargs: - raise_error(_('You cannot use keyword arguments in function {0}').format(func_name+u()+'()')) + raise_error(_('Cannot support keyword arguments')) if func_name == 'character': if _Parser.inlined_function_nodes['character'][0](args): @@ -942,7 +942,7 @@ class FormatterFuncsCaller(): if rslt is None: raise_error(_("Invalid character name '{0}'").format(args[0])) else: - raise_error(_('Incorrect number of arguments for function {0}').format(func_name+u()+'()')) + raise_error(_('Incorrect number of arguments')) # buildin/user function and Stored GPM/Python template func = formatter.funcs[func_name] @@ -955,7 +955,7 @@ class FormatterFuncsCaller(): # Change the error message to return this used name on the template e = e.__class__(_('Error in the function {0} :: {1}').format( func_name+u(), - re.sub(r'\w+\.evaluate\(\)', func_name+u()+'()', str(e), 1))) # replace UserFunction.evaluate() | Builtin*.evaluate() by the func name + re.sub(r'\w+\.evaluate\(\)\s*', '', str(e), 1))) # remove UserFunction.evaluate() | Builtin*.evaluate() e.is_internal = True raise e return rslt @@ -967,7 +967,9 @@ class FormatterFuncsCaller(): raise e def __dir__(self): - return list(set(object.__dir__(self) + list(self.__formatter__.funcs.keys()) + [f+'_' for f in self.__formatter__.funcs.keys()])) + return list(set(object.__dir__(self) + + list(self.__formatter__.funcs.keys()) + + [f+'_' for f in self.__formatter__.funcs.keys()])) class _Interpreter: From 56e482aeb1c1f21d47ffe86f5ddc085f213bdcd9 Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Sat, 15 Oct 2022 09:59:48 +0200 Subject: [PATCH 08/12] no need undersore, keyword arguments None value --- src/calibre/utils/formatter.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 2ab2a9dd1d..817b946b89 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -889,21 +889,16 @@ class FormatterFuncsCaller(): formatter = self.__formatter__ func_name = None - undersore = None if name.endswith('_') and name[:-1] in formatter.funcs: #given the priority to the backup name func_name = name[:-1] - undersore = True if not func_name and name in formatter.funcs: func_name = name - undersore = False if func_name: def call(*args, **kargs): args = [str(a) for a in args] - kargs = {k:str(v) for k,v in kargs.items()} + kargs = {k:str(v if v else '') for k,v in kargs.items()} - def u(): - return '_' if undersore else '' def raise_error(msg): raise ValueError(msg) def raise_mixed(args, kargs): @@ -954,7 +949,7 @@ class FormatterFuncsCaller(): 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( - func_name+u(), + name, re.sub(r'\w+\.evaluate\(\)\s*', '', str(e), 1))) # remove UserFunction.evaluate() | Builtin*.evaluate() e.is_internal = True raise e From 62cda3ea2f079ea0830053de5e926858a70d3d71 Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Sun, 16 Oct 2022 12:02:17 +0200 Subject: [PATCH 09/12] fix None args to empty --- src/calibre/utils/formatter.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 817b946b89..5d0820a623 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -896,8 +896,9 @@ class FormatterFuncsCaller(): if func_name: def call(*args, **kargs): - args = [str(a) for a in args] - kargs = {k:str(v if v else '') for k,v in kargs.items()} + args = [str('' if a is None else a) for a in args] + kargs = {k:str('' if v is None else v) for k,v in kargs.items()} + def raise_error(msg): raise ValueError(msg) @@ -1715,7 +1716,7 @@ class TemplateFormatter(string.Formatter): if getattr(e, 'is_internal', False): # Exception raised by FormatterFuncsCaller # get the line inside the current template instead of the FormatterFuncsCaller - for s in reversed(traceback.extract_tb(exc_info()[2])): + for s in reversed(stack): if s.filename == '': ss = s break From 983bd742ab448dd3983ca2ce009f3b29377d1f8b Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Sun, 16 Oct 2022 15:01:11 +0200 Subject: [PATCH 10/12] rearrange special function --- src/calibre/utils/formatter.py | 44 ++++++++++++++++------------------ 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 5d0820a623..501068d676 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -896,36 +896,34 @@ class FormatterFuncsCaller(): if func_name: def call(*args, **kargs): - args = [str('' if a is None else a) for a in args] - kargs = {k:str('' if v is None else v) for k,v in kargs.items()} + def n(d): + return str('' if d is None else d) + args = [n(a) for a in args] + kargs = {n(k):v for k,v in kargs.items()} - def raise_error(msg): raise ValueError(msg) - def raise_mixed(args, kargs): - if args and kargs: - raise_error(_('Invalid mixing keyword arguments and positional arguments')) try: # special function - if func_name == 'set_globals': - raise_mixed(args, kargs) + if func_name in ['arguments', 'globals', 'set_globals']: + if args and kargs: + raise_error(_('Invalid mixing keyword arguments and positional arguments')) + kargs.update({a:'' for a in args}) - formatter.global_vars.update(kargs) - rslt = kargs - - elif func_name == 'globals': - raise_mixed(args, kargs) - kargs.update({a:'' for a in args}) - rslt = {k:formatter.global_vars.get(k, d) for k,d in kargs.items()} - - elif func_name == 'arguments': - raise_mixed(args, kargs) - kargs.update({a:'' for a in args}) - args = formatter.python_context_object.arguments if formatter.python_context_object.arguments else [] - for i,k in enumerate(kargs.keys()): - if i == len(args): break - kargs[k] = str(args[i]) + + if func_name == 'arguments': + args = formatter.python_context_object.arguments or [] + for i,k in enumerate(kargs.keys()): + if i == len(args): break + kargs[k] = str(args[i]) + + elif func_name == 'globals': + kargs = {k:formatter.global_vars.get(k, d) for k,d in kargs.items()} + + elif func_name == 'set_globals': + formatter.global_vars.update(kargs) + rslt = kargs else: From e7cc30b3a9836035c0cbda4ceae8e140666168b2 Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Sun, 16 Oct 2022 15:01:39 +0200 Subject: [PATCH 11/12] add doc --- manual/template_lang.rst | 3 +++ src/calibre/gui2/dialogs/template_dialog.py | 1 + 2 files changed, 4 insertions(+) diff --git a/manual/template_lang.rst b/manual/template_lang.rst index f37d45859f..64add7f1e2 100644 --- a/manual/template_lang.rst +++ b/manual/template_lang.rst @@ -661,6 +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 # your Python code goes here return 'a string' @@ -669,6 +670,8 @@ 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. Note that all functions will return a string value, only the special functions ``arguments()``, ``globals()`` and ``set_globals()`` will return a dict based on the passed keywords arguments. + 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. .. code-block:: python diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index 2275192b36..f1e85315ed 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -593,6 +593,7 @@ def evaluate(book, context): # 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 # your Python code goes here return 'a string' From 02b28118929b9f6bab7954c47662bf61f6a886aa Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Mon, 17 Oct 2022 14:22:19 +0200 Subject: [PATCH 12/12] chaley recommandation --- manual/template_lang.rst | 2 +- src/calibre/utils/formatter.py | 57 ++++++++++++---------------------- 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/manual/template_lang.rst b/manual/template_lang.rst index 64add7f1e2..602a9306c9 100644 --- a/manual/template_lang.rst +++ b/manual/template_lang.rst @@ -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. Note that all functions will return a string value, only the special functions ``arguments()``, ``globals()`` and ``set_globals()`` will return a dict based on the passed keywords arguments. +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. 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 501068d676..3b016641ad 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -891,7 +891,7 @@ class FormatterFuncsCaller(): func_name = None if name.endswith('_') and name[:-1] in formatter.funcs: #given the priority to the backup name func_name = name[:-1] - if not func_name and name in formatter.funcs: + elif name in formatter.funcs: func_name = name if func_name: @@ -899,46 +899,29 @@ class FormatterFuncsCaller(): def n(d): return str('' if d is None else d) args = [n(a) for a in args] - kargs = {n(k):v for k,v in kargs.items()} - def raise_error(msg): - raise ValueError(msg) - try: - # special function - if func_name in ['arguments', 'globals', 'set_globals']: - if args and kargs: - raise_error(_('Invalid mixing keyword arguments and positional arguments')) - - kargs.update({a:'' for a in args}) - - if func_name == 'arguments': - args = formatter.python_context_object.arguments or [] - for i,k in enumerate(kargs.keys()): - if i == len(args): break - kargs[k] = str(args[i]) - - elif func_name == 'globals': - kargs = {k:formatter.global_vars.get(k, d) for k,d in kargs.items()} - - elif func_name == 'set_globals': - formatter.global_vars.update(kargs) - - rslt = kargs + def raise_error(msg): + raise ValueError(msg) + if kargs: + raise_error(_('Got an unsupported keyword argument')) + # 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': + if _Parser.inlined_function_nodes['character'][0](args): + rslt = _Interpreter.characters.get(args[0], None) + if rslt is None: + raise_error(_("Invalid character name '{0}'").format(args[0])) + else: + raise_error(_('Incorrect number of arguments')) else: - if kargs: - raise_error(_('Cannot support keyword arguments')) - - if func_name == 'character': - if _Parser.inlined_function_nodes['character'][0](args): - rslt = _Interpreter.characters.get(args[0], None) - if rslt is None: - raise_error(_("Invalid character name '{0}'").format(args[0])) - else: - raise_error(_('Incorrect number of arguments')) - - # buildin/user function and Stored GPM/Python template + # builtin/user function and Stored GPM/Python template func = formatter.funcs[func_name] if func.object_type == StoredObjectType.PythonFunction: rslt = func.evaluate(formatter, formatter.kwargs, formatter.book, formatter.locals, *args)