mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge branch 'python_template_introspection' of https://github.com/un-pogaz/calibre
This commit is contained in:
commit
8b270da20f
@ -661,6 +661,7 @@ A PTM template begins with:
|
|||||||
# db: a calibre legacy database object
|
# db: a calibre legacy database object
|
||||||
# globals: the template global variable dictionary
|
# globals: the template global variable dictionary
|
||||||
# arguments: is a list of arguments if the template is called by a GPM template, otherwise None
|
# 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
|
# your Python code goes here
|
||||||
return 'a string'
|
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 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.
|
||||||
|
|
||||||
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.
|
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
|
.. code-block:: python
|
||||||
|
@ -838,7 +838,9 @@ class PythonTemplateContext(object):
|
|||||||
self.db = None
|
self.db = None
|
||||||
self.arguments = None
|
self.arguments = None
|
||||||
self.globals = None
|
self.globals = None
|
||||||
self.attrs_set = {'db', 'arguments', 'globals'}
|
self.formatter = None
|
||||||
|
self.funcs = None
|
||||||
|
self.attrs_set = {'db', 'arguments', 'globals', 'funcs'}
|
||||||
|
|
||||||
def set_values(self, **kwargs):
|
def set_values(self, **kwargs):
|
||||||
# Create/set attributes from the named parameters. Doing it this way we
|
# Create/set attributes from the named parameters. Doing it this way we
|
||||||
@ -864,6 +866,89 @@ class PythonTemplateContext(object):
|
|||||||
return '\n'.join(f'{k}:{v}' for k,v in ans.items())
|
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):
|
||||||
|
object.__init__(self)
|
||||||
|
if not isinstance(formatter, TemplateFormatter):
|
||||||
|
raise ValueError('Class {} is not an instance of TemplateFormatter'
|
||||||
|
.format(formatter.__class__.__name__))
|
||||||
|
self.__formatter__ = formatter
|
||||||
|
|
||||||
|
def __getattribute__(self, name):
|
||||||
|
if name.startswith('__') and name.endswith('__'): # return internal special attribut
|
||||||
|
try:
|
||||||
|
return object.__getattribute__(self, name)
|
||||||
|
except:
|
||||||
|
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 = 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]
|
||||||
|
|
||||||
|
try:
|
||||||
|
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:
|
||||||
|
# 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)
|
||||||
|
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()
|
||||||
|
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) +
|
||||||
|
list(self.__formatter__.funcs.keys()) +
|
||||||
|
[f+'_' for f in self.__formatter__.funcs.keys()]))
|
||||||
|
|
||||||
|
|
||||||
class _Interpreter:
|
class _Interpreter:
|
||||||
def error(self, message, line_number):
|
def error(self, message, line_number):
|
||||||
m = _('Interpreter: {0} - line number {1}').format(message, line_number)
|
m = _('Interpreter: {0} - line number {1}').format(message, line_number)
|
||||||
@ -1490,6 +1575,7 @@ class TemplateFormatter(string.Formatter):
|
|||||||
self.funcs = formatter_functions().get_functions()
|
self.funcs = formatter_functions().get_functions()
|
||||||
self._interpreters = []
|
self._interpreters = []
|
||||||
self._template_parser = None
|
self._template_parser = None
|
||||||
|
self._caller = FormatterFuncsCaller(self)
|
||||||
self.recursion_stack = []
|
self.recursion_stack = []
|
||||||
self.recursion_level = -1
|
self.recursion_level = -1
|
||||||
|
|
||||||
@ -1601,10 +1687,21 @@ class TemplateFormatter(string.Formatter):
|
|||||||
self.python_context_object.set_values(
|
self.python_context_object.set_values(
|
||||||
db=get_database(self.book, get_database(self.book, None)),
|
db=get_database(self.book, get_database(self.book, None)),
|
||||||
globals=self.global_vars,
|
globals=self.global_vars,
|
||||||
arguments=arguments)
|
arguments=arguments,
|
||||||
|
formatter=self,
|
||||||
|
funcs=self._caller)
|
||||||
rslt = compiled_template(self.book, self.python_context_object)
|
rslt = compiled_template(self.book, self.python_context_object)
|
||||||
except Exception as e:
|
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 s in reversed(stack):
|
||||||
|
if s.filename == '<string>':
|
||||||
|
ss = s
|
||||||
|
break
|
||||||
|
|
||||||
raise ValueError(_('Error in function {0} on line {1} : {2} - {3}').format(
|
raise ValueError(_('Error in function {0} on line {1} : {2} - {3}').format(
|
||||||
ss.name, ss.lineno, type(e).__name__, str(e)))
|
ss.name, ss.lineno, type(e).__name__, str(e)))
|
||||||
if not isinstance(rslt, str):
|
if not isinstance(rslt, str):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user