Bug #1933591: Possible column template bug: Unknown identifier.

The problem: recursion triggered by field references of composite columns changed the state of the 'calling' interpreter.
This commit is contained in:
Charles Haley 2021-06-26 12:39:35 +01:00
parent 3247692512
commit ee26f5740f

View File

@ -1154,10 +1154,15 @@ class TemplateFormatter(string.Formatter):
self.book = None self.book = None
self.kwargs = None self.kwargs = None
self.strip_results = True self.strip_results = True
self.column_name = None
self.template_cache = None
self.global_vars = {}
self.locals = {} self.locals = {}
self.funcs = formatter_functions().get_functions() self.funcs = formatter_functions().get_functions()
self.gpm_parser = _Parser() self._interpreters = []
self.gpm_interpreter = _Interpreter() self._template_parser = None
self.recursion_stack = []
self.recursion_level = -1
def _do_format(self, val, fmt): def _do_format(self, val, fmt):
if not fmt or not val: if not fmt or not val:
@ -1333,17 +1338,77 @@ class TemplateFormatter(string.Formatter):
ans = ans.strip(' ') ans = ans.strip(' ')
return ans return ans
# It is possible for a template to indirectly invoke other templates by
# doing field references of composite columns. If this happens then the
# reference can use different parameters when calling safe_format(). Because
# the parameters are saved as instance variables they can possibly affect
# the 'calling' template. To avoid this problem, save the current formatter
# state when recursion is detected. There is no point in saving the level
# 0 state.
def save_state(self):
self.recursion_level += 1
if self.recursion_level > 0:
return (
(self.strip_results,
self.column_name,
self.template_cache,
self.kwargs,
self.book,
self.global_vars,
self.funcs,
self.locals))
else:
return None
def restore_state(self, state):
self.recursion_level -= 1
if state is not None:
(self.strip_results,
self.column_name,
self.template_cache,
self.kwargs,
self.book,
self.global_vars,
self.funcs,
self.locals) = state
# Allocate an interpreter if the formatter encounters a GPM or TPM template.
# We need to allocate additional interpreters if there is composite recursion
# so that the templates are evaluated by separate instances. It is OK to
# reuse already-allocated interpreters because their state is initialized on
# call. As a side effect, no interpreter is instantiated if no TPM/GPM
# template is encountered.
@property
def gpm_interpreter(self):
if len(self._interpreters) <= self.recursion_level:
self._interpreters.append(_Interpreter())
return self._interpreters[self.recursion_level]
# Allocate a parser if needed. Parsers cannot recurse so one is sufficient.
@property
def gpm_parser(self):
if self._template_parser == None:
self._template_parser = _Parser()
return self._template_parser
# ######### a formatter that throws exceptions ############ # ######### a formatter that throws exceptions ############
def unsafe_format(self, fmt, kwargs, book, strip_results=True, global_vars=None): def unsafe_format(self, fmt, kwargs, book, strip_results=True, global_vars=None):
self.strip_results = strip_results state = self.save_state()
self.column_name = self.template_cache = None try:
self.kwargs = kwargs self.strip_results = strip_results
self.book = book self.column_name = self.template_cache = None
self.composite_values = {} self.kwargs = kwargs
self.locals = {} self.book = book
self.global_vars = global_vars if isinstance(global_vars, dict) else {} self.composite_values = {}
return self.evaluate(fmt, [], kwargs, self.global_vars) self.locals = {}
self.global_vars = global_vars if isinstance(global_vars, dict) else {}
return self.evaluate(fmt, [], kwargs, self.global_vars)
finally:
self.restore_state(state)
# ######### a formatter guaranteed not to throw an exception ############ # ######### a formatter guaranteed not to throw an exception ############
@ -1351,30 +1416,36 @@ class TemplateFormatter(string.Formatter):
column_name=None, template_cache=None, column_name=None, template_cache=None,
strip_results=True, template_functions=None, strip_results=True, template_functions=None,
global_vars=None, break_reporter=None): global_vars=None, break_reporter=None):
self.strip_results = strip_results state = self.save_state()
self.column_name = column_name if self.recursion_level == 0:
self.template_cache = template_cache # Initialize the composite values dict if this is the base-level
self.kwargs = kwargs # call. Recursive calls will use the same dict.
self.book = book self.composite_values = {}
self.global_vars = global_vars if isinstance(global_vars, dict) else {}
if template_functions:
self.funcs = template_functions
else:
self.funcs = formatter_functions().get_functions()
self.composite_values = {}
self.locals = {}
try: try:
ans = self.evaluate(fmt, [], kwargs, self.global_vars, self.strip_results = strip_results
break_reporter=break_reporter) self.column_name = column_name
except StopException as e: self.template_cache = template_cache
ans = error_message(e) self.kwargs = kwargs
except Exception as e: self.book = book
if DEBUG: # and getattr(e, 'is_locking_error', False): self.global_vars = global_vars if isinstance(global_vars, dict) else {}
traceback.print_exc() if template_functions:
if column_name: self.funcs = template_functions
prints('Error evaluating column named:', column_name) else:
ans = error_value + ' ' + error_message(e) self.funcs = formatter_functions().get_functions()
return ans self.locals = {}
try:
ans = self.evaluate(fmt, [], kwargs, self.global_vars, break_reporter=break_reporter)
except StopException as e:
ans = error_message(e)
except Exception as e:
if DEBUG: # and getattr(e, 'is_locking_error', False):
traceback.print_exc()
if column_name:
prints('Error evaluating column named:', column_name)
ans = error_value + ' ' + error_message(e)
return ans
finally:
self.restore_state(state)
class ValidateFormatter(TemplateFormatter): class ValidateFormatter(TemplateFormatter):