[IMP] base: Qweb add t-cache and t-nocache directive

The `t-cache` directive allows you to keep the rendered result
of a template part. The supplied key must be a tuple. This tuple
can contain recordset in this case the zone will be invalidated
each time the write_date of these records changes.

The `t-nocache` directive makes it possible to force rendering
of a part even if it is in a `t-cache`. The values available in
the `t-nocache` are the one provided when calling the template
(and therefore ignores any t-set that could have been done).

Part-of: odoo/odoo#88276
This commit is contained in:
Gorash
2022-06-03 16:40:40 +02:00
parent 8c869769e9
commit 9af92b7374
7 changed files with 1725 additions and 377 deletions
+1 -1
View File
@@ -382,7 +382,7 @@ class Http(models.AbstractModel):
# contains branding (<div t-att-data="request.browse('ok')"/>).
et = view.with_context(inherit_branding=False)._get_combined_arch()
node = et.xpath(exception.path) if exception.path else et
line = node is not None and etree.tostring(node[0], encoding='unicode')
line = node is not None and len(node) > 0 and etree.tostring(node[0], encoding='unicode')
if line:
values['view'] = View._views_get(exception_template).filtered(
lambda v: line in v.arch
+395 -155
View File
@@ -1,5 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
"""
================
IrQWeb / ir.qweb
@@ -77,8 +76,11 @@ in the IrQweb class.
┣━► _compile_directives (loop) Consume all remaining directives ◄━━━┓ ┃ ┃
┃ ┃ (e.g.: to change the indentation) ┃ ┃ ┃
┃ ┣━► _compile_directive ┃ ┃ ┃
┃ ┃ ┗━► t-if ━━► _compile_directive_if ━┫ ┃ ┃
┃ ┃ ┗━► t-foreach ━━► _compile_directive_foreach ━┛ ┃ ┃
┃ ┃ ┗━► t-nocache ━━► _compile_directive_nocache ━┫ ┃ ┃
┃ ┃ ┗━► t-cache ━━► _compile_directive_cache ━┫ ┃ ┃
┃ ┃ ┗━► t-groups ━━► _compile_directive_groups ━┫ ┃ ┃
┃ ┃ ┗━► t-foreach ━━► _compile_directive_foreach ━┫ ┃ ┃
┃ ┃ ┗━► t-if ━━► _compile_directive_if ━┛ ┃ ┃
┃ ┃ ┗━► t-inner-content ━━► _compile_directive_inner_content ◄━━━━━┓ ━┛ ┃
┃ ┃ ┗━► t-options ━━► _compile_directive_options ┃ ┃
┃ ┃ ┗━► t-set ━━► _compile_directive_set ◄━━┓ ┃ ┃
@@ -322,16 +324,6 @@ same node.
The compilation method only validates if ``t-valuef`` and ``t-set`` are on the
same node.
``t-set-*`` and ``t-setf-*``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Values**: python expression or format string expression
For each key-value ``t-set-key="value"`` the value will be compiled into a
python expression or into a format string expression for ``t-setf-*``.
At **rendering time** the ``values`` dictionary is updated.
(for example: ``t-set-bib="1 + 1"`` is equal to ``t-set="bib" t-value="2"``)
Technical directives
--------------------
@@ -382,8 +374,8 @@ from markupsafe import Markup, escape
from collections.abc import Sized, Mapping
from itertools import count, chain
from lxml import etree
from psycopg2.extensions import TransactionRollbackError
from dateutil.relativedelta import relativedelta
from psycopg2.extensions import TransactionRollbackError
from odoo import api, models, tools
from odoo.tools import config, safe_eval, pycompat, SUPPORTED_DEBUGGER
@@ -394,7 +386,7 @@ from odoo.tools.image import image_data_uri
from odoo.http import request
from odoo.modules.module import get_resource_path
from odoo.tools.profiler import QwebTracker
from odoo.exceptions import UserError
from odoo.exceptions import UserError, AccessDenied, AccessError, MissingError, ValidationError
from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.base.models.ir_asset import can_aggregate, STYLE_EXTENSIONS, SCRIPT_EXTENSIONS
@@ -435,7 +427,7 @@ ALLOWED_KEYWORD = frozenset(['False', 'None', 'True', 'and', 'as', 'elif', 'else
# regexpr for string formatting and extract ( ruby-style )|( jinja-style ) used in `_compile_format`
FORMAT_REGEX = re.compile(r'(?:#\{(.+?)\})|(?:\{\{(.+?)\}\})')
RSTRIP_REGEXP = re.compile(r'\n[ \t]*$')
FISRT_RSTRIP_REGEXP = re.compile(r'^(\n[ \t]*)+(\n[ \t])')
FIRST_RSTRIP_REGEXP = re.compile(r'^(\n[ \t]*)+(\n[ \t])')
VARNAME_REGEXP = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$')
TO_VARNAME_REGEXP = re.compile(r'[^A-Za-z0-9_]+')
# Attribute name used outside the context of the QWeb.
@@ -479,29 +471,35 @@ def keep_query(*keep_params, **additional_params):
class QWebException(Exception):
""" Management of errors that raised when rendering a QWeb template.
"""
def __init__(self, message, qweb, template=None, path=None, code=None):
self.name = template
self.code = code if qweb.env.context.get('dev_mode') else None
self.path = path
self.html = None
if template is not None and path and ':' not in path:
try:
element = qweb._get_template(template)[0]
nodes = element.getroottree().xpath(self.path)
if nodes:
node = etree.Element(nodes[0].tag, nodes[0].attrib)
self.html = etree.tostring(node, encoding='unicode')
except Exception as e:
_logger.warning('Cannot load template %s: %s', template, e)
def __init__(self, message, qweb, template=None, ref=None, path_xml=None, code=None):
self.stack = traceback.format_exc()
self.name = template
self.ref = ref
self.path, self.html = path_xml or (None, None)
self.code = None
if code:
self.code = '\n'.join(code.split('\n')[:-1]) if qweb.env.context.get('dev_mode') else None
line_nb = 0
for error_line in reversed(self.stack.split('\n')):
if f'File "<{self.ref}>"' in error_line:
line_function = error_line.split(', line ')[1]
line_nb = int(line_function.split(',')[0])
break
for code_line in reversed(code.split('\n')[:line_nb]):
match = re.match(r'\s*# element: (.*) , (.*)', code_line)
if match:
self.path = match[1][1:-1]
self.html = match[2][1:-1]
break
self.title = message
super().__init__(message)
def __str__(self):
parts = [self.title]
if str(self.__cause__) != '':
if self.__cause__ and str(self.__cause__) != '':
parts.append(f"{self.__cause__.__class__.__name__}: {self.__cause__}")
elif str(self.__context__) != '':
elif self.__context__ and str(self.__context__) != '':
parts.append(f"{self.__context__.__class__.__name__}: {self.__context__}")
if self.name is not None:
parts.append(f"Template: {self.name}")
@@ -545,7 +543,6 @@ class IrQWeb(models.AbstractModel):
:param dict values: template values to be used for rendering
:param options: used to compile the template
Options will be add into the IrQweb.env.context for the rendering.
* ``load`` (function) overrides the load method
* ``lang`` (str) used language to render the template
* ``inherit_branding`` (bool) add the tag node branding
* ``inherit_branding_auto`` (bool) add the branding on fields
@@ -558,13 +555,14 @@ class IrQWeb(models.AbstractModel):
"""
values = values.copy() if values else {}
if T_CALL_SLOT in values:
raise ValueError('values[0] should be unset when call the _render method and only set into the template.')
raise ValueError(f'values[{T_CALL_SLOT}] should be unset when call the _render method and only set into the template.')
irQweb = self.with_context(**options)._prepare_environment(values)
safe_eval.check_values(values)
render_template = irQweb._compile(template)
template_functions, def_name = irQweb._compile(template)
render_template = template_functions[def_name]
rendering = render_template(irQweb, values)
result = ''.join(rendering)
@@ -575,16 +573,59 @@ class IrQWeb(models.AbstractModel):
""" Return the list of context keys to use for caching ``_compile``. """
return ['lang', 'inherit_branding', 'edit_translations', 'profile']
# apply ormcache_context decorator unless in dev mode...
@tools.conditional(
'xml' not in tools.config['dev_mode'],
tools.ormcache('template', 'tuple(self.env.context.get(k) for k in self._get_template_cache_keys())'),
)
def _get_view_id(self, template):
try:
return self.env['ir.ui.view'].sudo().with_context(load_all_views=True)._get_view_id(template)
except Exception:
return None
@QwebTracker.wrap_compile
def _compile(self, template):
if isinstance(template, etree._Element):
self = self.with_context(is_t_cache_disabled=True)
ref = None
else:
ref = self._get_view_id(template)
# define the base key cache for code in cache and t-cache feature
base_key_cache = None
if ref:
base_key_cache = self._get_cache_key(tuple([ref] + [self.env.context.get(k) for k in self._get_template_cache_keys()]))
self = self.with_context(__qweb_base_key_cache=base_key_cache)
# generate the template functions and the root function name
def generate_functions():
code, options, def_name = self._generate_code(template)
code = '\n'.join([
"def generate_functions():",
" template_functions = {}",
indent_code(code, 1),
f" template_functions['options'] = {options if self.env.context.get('profile') else None!r}",
" return template_functions",
])
try:
compiled = compile(code, f"<{ref}>", 'exec')
globals_dict = self._prepare_globals()
globals_dict['__builtins__'] = globals_dict # So that unknown/unsafe builtins are never added.
unsafe_eval(compiled, globals_dict)
return globals_dict['generate_functions'](), def_name
except QWebException:
raise
except Exception as e:
raise QWebException("Error when compiling xml template",
self, template, code=code, ref=ref) from e
return self._load_values(base_key_cache, generate_functions)
def _generate_code(self, template):
""" Compile the given template into a rendering function (generator)::
render(qweb, values)
render_template(qweb, values)
This method can be called only by the IrQweb `_render` method or by
the compiled code of t-call from an other template.
@@ -594,6 +635,8 @@ class IrQWeb(models.AbstractModel):
where ``qweb`` is a QWeb instance and ``values`` are the values to
render.
:returns: tuple containing code, options and main method name
"""
# The `compile_context`` dictionary includes the elements used for the
# cache key to which are added the template references as well as
@@ -604,14 +647,18 @@ class IrQWeb(models.AbstractModel):
try:
element, document, ref = self._get_template(template)
except (ValueError, UserError) as e:
# return the error function if the template is not found or fail
message = str(e)
ClassError = e.__class__
def not_found_template(self, values, gen0=None):
if self.env.context.get('raise_if_not_found', True):
raise ClassError(message)
_logger.warning('Cannot load template %s: %s', template, message)
return ''
return not_found_template
code = indent_code(f"""
def not_found_template(self, values):
if self.env.context.get('raise_if_not_found', True):
raise {e.__class__.__name__}({message!r})
warning('Cannot load template %s: %s', {template!r}, {message!r})
return ''
template_functions = {{'not_found_template': not_found_template}}
""", 0)
return (code, {}, 'not_found_template')
compile_context.pop('raise_if_not_found', None)
# reference to get xml and etree (usually the template ID)
@@ -625,7 +672,7 @@ class IrQWeb(models.AbstractModel):
# Root of the etree which will be processed during compilation.
compile_context['root'] = element.getroottree()
# Reference to the last node being compiled. It is mainly used for debugging and displaying error messages.
compile_context['last_path_node'] = None
compile_context['_qweb_error_path_xml'] = None
if not compile_context.get('nsmap'):
compile_context['nsmap'] = {}
@@ -639,72 +686,63 @@ class IrQWeb(models.AbstractModel):
# generate code
compile_context['qweb_name_gen'] = count()
def_name = TO_VARNAME_REGEXP.sub(r'_', f'template_{ref}')
def_name = self._make_name(TO_VARNAME_REGEXP.sub(r'_', f'template_{ref}'), compile_context)
name_gen = count()
compile_context['make_name'] = lambda prefix: f"{def_name}_{prefix}_{next(name_gen)}"
try:
if element.text:
element.text = FISRT_RSTRIP_REGEXP.sub(r'\2', element.text)
element.text = FIRST_RSTRIP_REGEXP.sub(r'\2', element.text)
compile_context['template_functions'] = {}
compile_context['_all_def'] = []
compile_context['_text_concat'] = []
self._append_text("", compile_context) # To ensure the template function is a generator and doesn't become a regular function
code_lines = (
[f'def {def_name}(self, values, gen0, log):']
+ self._compile_node(element, compile_context, 1)
+ self._flush_text(compile_context, 1, rstrip=True)
)
for lines in compile_context['_all_def']:
code_lines.extend(lines)
compile_context['template_functions'][f'{def_name}_content'] = (
[f"def {def_name}_content(self, values):"]
+ self._compile_node(element, compile_context, 2)
+ self._flush_text(compile_context, 2, rstrip=True))
compile_context['template_functions'][def_name] = [indent_code(f"""
def {def_name}(self, values):
try:
if '__qweb_loaded_values' not in values:
values['__qweb_loaded_values'] = {{}}
values['__qweb_root_values'] = values.copy()
values['xmlid'] = {options['ref_name']!r}
values['viewid'] = {options['ref']!r}
values['__qweb_loaded_values'].update(template_functions)
yield from {def_name}_content(self, values)
except QWebException:
raise
except Exception as e:
if isinstance(e, TransactionRollbackError):
raise
raise QWebException("Error while render the template",
self, template, ref={compile_context['ref']!r}, code=code) from e
""", 0)]
except QWebException:
raise
except Exception as e:
raise QWebException("Error when compiling xml template",
self, template, path=compile_context.get('last_path_node')) from e
try:
code = '\n'.join(code_lines)
except Exception as e:
code = '\n'.join(map(str, code_lines))
raise QWebException("Error when compiling xml template",
self, template, code=code) from e
self, template, ref=compile_context['ref'], path_xml=compile_context['_qweb_error_path_xml']) from e
# compile code and defined default values
code_lines = ['code = None']
code_lines.append(f'template = {(document if isinstance(template, etree._Element) else template)!r}')
code_lines.append('template_functions = {}')
try:
# noinspection PyBroadException
compiled = compile(code, f'<{def_name}>', 'exec')
globals_dict = self._prepare_globals()
globals_dict['__builtins__'] = globals_dict # So that unknown/unsafe builtins are never added.
unsafe_eval(compiled, globals_dict)
compiled_fn = globals_dict[def_name]
except QWebException:
raise
except Exception as e:
raise QWebException("Error when compiling xml template",
self, template, code=code) from e
for lines in compile_context['template_functions'].values():
code_lines.extend(lines)
# return the wrapped function
for name in compile_context['template_functions']:
code_lines.append(f'template_functions[{name!r}] = {name}')
def render_template(self, values, gen0=None):
try:
if not values.get('xmlid'):
values['xmlid'] = options['ref_name']
values['viewid'] = options['ref']
log = {'last_path_node': None}
if gen0 is None:
gen0 = []
yield from compiled_fn(self, values, gen0, log)
except (QWebException, TransactionRollbackError) as e:
raise
except Exception as e:
raise QWebException("Error while render the template",
self, template, path=log.get('last_path_node'), code=code) from e
code = '\n'.join(code_lines)
code += f'\n\ncode = {code!r}'
# add key cache options for profiling tools
render_template.options = options
return render_template
return (code, options, def_name)
# read and load input template
@@ -715,7 +753,6 @@ class IrQWeb(models.AbstractModel):
reference of the template (id, t-name or template).
:param template: template identifier or etree
:param self.env.context['load']: (function(template)) overrides the load method
"""
assert template not in (False, None, ""), "template is required"
@@ -735,7 +772,7 @@ class IrQWeb(models.AbstractModel):
except ValueError:
ref_alias = template # e.g. web.layout
doc_or_elem, ref = self.env.context.get('load', self._load)(ref_alias) or (None, None)
doc_or_elem, ref = self._load(ref_alias) or (None, None)
if doc_or_elem is None:
raise ValueError(f"Can not load template: {ref_alias!r}")
if isinstance(doc_or_elem, etree._Element):
@@ -807,12 +844,13 @@ class IrQWeb(models.AbstractModel):
:returns self (with new context)
"""
debug = request and request.session.debug or ''
values.update(
true=True,
false=False,
)
if not self.env.context.get('minimal_qcontext'):
values.setdefault('debug', request and request.session.debug or '')
values.setdefault('debug', debug)
values.setdefault('user_id', self.env.user.with_env(self.env))
values.setdefault('res_company', self.env.company.sudo())
@@ -833,7 +871,12 @@ class IrQWeb(models.AbstractModel):
keep_query=keep_query,
)
return self.with_context(dev_mode='qweb' in tools.config['dev_mode'])
context = {'dev_mode': 'qweb' in tools.config['dev_mode']}
if 'xml' in tools.config['dev_mode']:
context['is_t_cache_disabled'] = True
elif 'disable-t-cache' in debug:
context['is_t_cache_disabled'] = True
return self.with_context(**context)
def _prepare_globals(self):
""" Prepare the global context that will sent to eval the qweb
@@ -845,6 +888,16 @@ class IrQWeb(models.AbstractModel):
'Markup': Markup,
'escape': escape,
'VOID_ELEMENTS': VOID_ELEMENTS,
'QWebException': QWebException,
'Exception': Exception,
'TransactionRollbackError': TransactionRollbackError, # for SerializationFailure in assets
'ValueError': ValueError,
'UserError': UserError,
'AccessDenied': AccessDenied,
'AccessError': AccessError,
'MissingError': MissingError,
'ValidationError': ValidationError,
'warning': lambda *args: _logger.warning(*args),
**_BUILTINS,
}
@@ -889,10 +942,6 @@ class IrQWeb(models.AbstractModel):
text_concat.clear()
return [f"{' ' * level}yield {text!r}"]
def _make_name(self, prefix, compile_context):
"""Generates a unique name."""
return f"{prefix}_{next(compile_context['qweb_name_gen'])}"
def _is_static_node(self, el, compile_context):
""" Test whether the given element is purely static, i.e. (there
are no t-* attributes), does not require dynamic rendering for its
@@ -1142,6 +1191,8 @@ class IrQWeb(models.AbstractModel):
'elif', # Must be the first because compiled by the previous if.
'else', # Must be the first because compiled by the previous if.
'debug',
'nocache',
'cache',
'groups',
'as', 'foreach',
'if',
@@ -1182,11 +1233,9 @@ class IrQWeb(models.AbstractModel):
return self._compile_static_node(el, compile_context, level)
path = compile_context['root'].getpath(el)
if compile_context['last_path_node'] != path:
compile_context['last_path_node'] = path
body = [indent_code(f'log["last_path_node"] = {path!r}', level)]
else:
body = []
xml = etree.tostring(etree.Element(el.tag, el.attrib), encoding='unicode')
compile_context['_qweb_error_path_xml'] = (path, xml)
body = [indent_code(f'# element: {path!r} , {xml!r}', level)]
# create an iterator on directives to compile in order
compile_context['iter_directives'] = iter(self._directives_eval_order())
@@ -1312,10 +1361,8 @@ class IrQWeb(models.AbstractModel):
elif directive == 'options':
if any(name.startswith('t-options-') for name in el.attrib):
code.extend(self._compile_directive(el, compile_context, directive, level))
elif directive == 'set':
if any(name.startswith('t-set-') or
name.startswith('t-setf-')
for name in el.attrib):
elif directive == 'nocache':
if any(name.startswith('t-nocache-') for name in el.attrib):
code.extend(self._compile_directive(el, compile_context, directive, level))
# compile unordered directives still present on the element
@@ -1346,7 +1393,7 @@ class IrQWeb(models.AbstractModel):
debugger = el.attrib.pop('t-debug')
code = []
if compile_context.get('dev_mode'):
code.append(indent_code(f"self._debug_trace({debugger!r})", level))
code.append(indent_code(f"self._debug_trace({debugger!r}, values)", level))
else:
_logger.warning("@t-debug in template is only available in qweb dev mode")
return code
@@ -1553,26 +1600,28 @@ class IrQWeb(models.AbstractModel):
self._compile_directive(el, compile_context, 'inner-content', 1) +
self._flush_text(compile_context, 1))
if content:
def_name = self._make_name('t_set', compile_context)
compile_context['_all_def'].append([f"def {def_name}(self, values, gen0, log):"] + content)
expr = f"Markup(''.join({def_name}(self, values, gen0, log)))"
def_name = compile_context['make_name']('t_set')
compile_context['template_functions'][def_name] = [f"def {def_name}(self, values):"] + content
code.append(indent_code(f"""
t_set = []
for item in {def_name}(self, values):
if isinstance(item, str):
t_set.append(item)
else:
ref, function_name, cached_values = item
t_nocache_function = values['__qweb_loaded_values'].get(function_name)
if not t_nocache_function:
t_call_template_functions, def_name = self._compile(ref)
t_nocache_function = t_call_template_functions[function_name]
nocache_values = values['__qweb_root_values'].copy()
nocache_values.update(cached_values)
t_set.extend(t_nocache_function(self, nocache_values))
""", level))
expr = "Markup(''.join(t_set))"
else:
expr = "''"
if varname == T_CALL_SLOT:
code.append(indent_code(f"gen0 = {expr}", level))
else:
code.append(indent_code(f"values[{varname!r}] = {expr}", level))
for key in list(el.attrib):
if key.startswith('t-set-') or key.startswith('t-setf-'):
expr = el.attrib.pop(key)
varname = key.split('-', 2)
if not VARNAME_REGEXP.match(varname[2]):
raise ValueError(f'The varname {varname[2]!r} can only contain alphanumeric characters and underscores.')
if varname[1] == 'set':
code.append(indent_code(f"values[{varname[2]!r}] = {self._compile_expr(expr)}", level))
else:
code.append(indent_code(f"values[{varname[2]!r}] = {self._compile_format(expr)}", level))
code.append(indent_code(f"values[{varname!r}] = {expr}", level))
return code
@@ -1769,9 +1818,9 @@ class IrQWeb(models.AbstractModel):
self._compile_directives(el, compile_context, level + 1) +
self._flush_text(compile_context, level + 1, rstrip=True))
t_foreach = self._make_name('t_foreach', compile_context)
size = self._make_name('size', compile_context)
has_value = self._make_name('has_value', compile_context)
t_foreach = compile_context['make_name']('t_foreach')
size = compile_context['make_name']('size')
has_value = compile_context['make_name']('has_value')
if expr_foreach.isdigit():
code.append(indent_code(f"""
@@ -1810,7 +1859,6 @@ class IrQWeb(models.AbstractModel):
values[{expr_as + '_parity'!r}] = 'odd' if values[{expr_as + '_odd'!r}] else 'even'
""", level))
code.append(indent_code(f'log["last_path_node"] = {compile_context["root"].getpath(el)!r} ', level + 1))
code.extend(content_foreach or indent_code('continue', level + 1))
return code
@@ -1876,7 +1924,7 @@ class IrQWeb(models.AbstractModel):
if expr == T_CALL_SLOT and code_options != 'True':
code.append(indent_code("if True:", level))
code.extend(tag_open)
code.append(indent_code("yield from gen0", level + 1))
code.append(indent_code(f"yield from values.get({T_CALL_SLOT}, [])", level + 1))
code.extend(tag_close)
return code
elif ttype == 't-field':
@@ -1894,7 +1942,7 @@ class IrQWeb(models.AbstractModel):
force_display_dependent = True
else:
if expr == T_CALL_SLOT:
code.append(indent_code("content = Markup(''.join(gen0))", level))
code.append(indent_code(f"content = Markup(''.join(values.get({T_CALL_SLOT}, [])))", level))
else:
code.append(indent_code(f"content = {self._compile_expr(expr)}", level))
@@ -2027,11 +2075,7 @@ class IrQWeb(models.AbstractModel):
# options
el.attrib.pop('t-consumed-options', None)
code.append(indent_code(f"""
t_call_options = dict(values.pop('__qweb_options__', {{}}))
t_call_options['caller_template'] = {str(compile_context.get('template'))!r}
t_call_options['last_path_node'] = {str(compile_context['root'].getpath(el))!r}
""", level))
code.append(indent_code("t_call_options = values.pop('__qweb_options__', {})", level))
if nsmap:
# update this dict with the current nsmap so that the callee know
# if he outputting the xmlns attributes is relevenat or not
@@ -2043,27 +2087,32 @@ class IrQWeb(models.AbstractModel):
nsmap.append(f'None:{value!r}')
code.append(indent_code(f"t_call_options.update(nsmap={{{', '.join(nsmap)}}})", level))
# values (t-out="0" from content and variables from t-set and t-set-*)
def_name = self._make_name('t_call', compile_context)
# values (t-out="0" from content and variables from t-set)
def_name = compile_context['make_name']('t_call')
# values from content (t-out="0" and t-set inside the content)
code_content = [f"def {def_name}(self, values, gen0, log):"]
code_content = [f"def {def_name}(self, values):"]
code_content.extend(self._compile_directive(el, compile_context, 'inner-content', 1))
code_content.extend(self._compile_directive(el, compile_context, 'set', 1))
self._append_text('', compile_context) # To ensure the template function is a generator and doesn't become a regular function
code_content.extend(self._flush_text(compile_context, 1, rstrip=True))
compile_context['_all_def'].append(code_content)
compile_context['template_functions'][def_name] = code_content
code.append(indent_code("t_call_values = values.copy()", level))
code.append(indent_code(f"t_call_gen0 = Markup(''.join({def_name}(self, t_call_values, gen0, log)))", level))
code.append(indent_code(f"""
t_call_values = values.copy()
t_call_values[{T_CALL_SLOT}] = list({def_name}(self, t_call_values))
""", level))
template = self._compile_format(expr)
# call
code.append(indent_code(f"""
irQweb = self.with_context(**t_call_options)
render_template = irQweb._compile({template})
yield from render_template(irQweb, t_call_values, t_call_gen0)
template = {template}
if template.isnumeric():
template = int(template)
t_call_template_functions, def_name = irQweb._compile(template)
render_template = t_call_template_functions[def_name]
yield from render_template(irQweb, t_call_values)
""", level))
return code
@@ -2125,10 +2174,147 @@ class IrQWeb(models.AbstractModel):
return code
def _compile_directive_cache(self, el, compile_context, level):
"""Compile the `t-cache` tuple expression into a key cache.
The `t-cache` directive allows you to keep the rendered result
of a template part. The supplied key must be a tuple. This tuple
can contain recordset in this case the zone will be invalidated
each time the write_date of these records changes.
The values are scoped into the `t-cache` and are not available
outside.
see: `t-nocache`
"""
expr = el.attrib.pop('t-cache')
code = self._flush_text(compile_context, level)
def_name = compile_context['make_name']('t_cache')
# Generate the content function
def_code = [indent_code(f"""def {def_name}(self, values):""", 0)]
def_content = self._compile_directives(el, compile_context, 1)
if def_content and not compile_context['_text_concat']:
self._append_text('', compile_context) # To ensure the template function is a generator and doesn't become a regular function
def_code.extend(def_content)
def_code.extend(self._flush_text(compile_context, 1))
compile_context['template_functions'][def_name] = def_code
# Get the dynamic key for the cache and load the content.
# The t-nocache yield a tuple (ref, function name) instead of a
# When reading tuple coming from t-nocache, we check if the
# method is already known otherwise the corresponding template
# and its functions are loaded.
code.append(indent_code(f"""
template_cache_key = {self._compile_expr(expr)} if not self.env.context.get('is_t_cache_disabled') else None
cache_key = self._get_cache_key(template_cache_key) if template_cache_key else None
uniq_cache_key = cache_key and ({str(self.env.context['__qweb_base_key_cache'])!r}, '{def_name}_cache', cache_key)
loaded_values = values['__qweb_loaded_values']
def {def_name}_cache():
content = []
text = []
for item in {def_name}(self, {{**values, '__qweb_in_cache': True}}):
if isinstance(item, str):
text.append(item)
else:
content.append(''.join(text))
content.append(item)
text = []
if text:
content.append(''.join(text))
return content
cache_content = self._load_values(uniq_cache_key, {def_name}_cache, loaded_values)
if values.get('__qweb_in_cache'):
yield from cache_content
else:
for item in cache_content:
if isinstance(item, str):
yield item
else:
ref, function_name, cached_values = item
t_nocache_function = loaded_values.get(function_name)
if not t_nocache_function:
t_call_template_functions, def_name = self._compile(ref)
t_nocache_function = t_call_template_functions[function_name]
nocache_values = values['__qweb_root_values'].copy()
nocache_values.update(cached_values)
yield ''.join(t_nocache_function(self, nocache_values))
""", level))
return code
def _compile_directive_nocache(self, el, compile_context, level):
"""
The `t-nocache` directive makes it possible to force rendering
of a part even if it is in a `t-cache`. The values available in
the `t-nocache` are the one provided when calling the template
(and therefore ignores any t-set that could have been done).
The `t-nocache-*` are the values whose result of the
expression will be cached and added to the root's values when
rendering the no cache part. Only primitive types can be cached.
see: `t-cache`
"""
if 't-nocache' not in el.attrib:
raise SyntaxError("t-nocache-* must be on the same node as t-nocache")
el.attrib.pop('t-nocache')
code = self._flush_text(compile_context, level)
# t-nocache-* will generate the values to put in cache
# must cosume this attributes before generate the cached content.
code_cache_values = []
for key in list(el.attrib):
if key.startswith('t-nocache-'):
expr = el.attrib.pop(key)
varname = key[10:]
if not VARNAME_REGEXP.match(varname):
raise ValueError(f'The varname {varname!r} can only contain alphanumeric characters and underscores.')
code_cache_values.append(indent_code(f"""
cached_value = {self._compile_expr(expr)}
if cached_value is not None and not isinstance(cached_value, (str, int, float, bool)):
raise ValueError(f'''The value type of {key!r} cannot be cached: {{cached_value!r}}''')
cached_values[{varname!r}] = cached_value
""", level + 1))
# generate the cached content method
def_name = compile_context['make_name']('t_nocache')
def_code = [f"def {def_name}(self, values):"]
def_code.append(indent_code("try:", 1))
def_content = self._compile_directives(el, compile_context, 2)
if def_content and not compile_context['_text_concat']:
self._append_text('', compile_context) # To ensure the template function is a generator and doesn't become a regular function
def_code.extend(def_content)
def_code.extend(self._flush_text(compile_context, 2))
def_code.append(indent_code(f"""
except QWebException:
raise
except Exception as e:
raise QWebException("Error while render the template",
self, template, ref={compile_context['ref']!r}, code=code) from e
""", 1))
compile_context['template_functions'][def_name] = def_code
# if the nocache is inside a cache return a tuple with the method name and the cached values
code.append(indent_code("""
if values.get('__qweb_in_cache'):
cached_values = {}
""", level))
code.extend(code_cache_values)
code.append(indent_code(f"yield ({compile_context['template']!r}, {def_name!r}, cached_values)", level+1))
# else render the content
code.append(indent_code(f"""
else:
yield from {def_name}(self, values)
""", level))
return code
# methods called by the compiled function at rendering time.
def _debug_trace(self, debugger):
"""Method called at compile time to load debugger."""
def _debug_trace(self, debugger, values):
"""Method called at running time to load debugger."""
if debugger in SUPPORTED_DEBUGGER:
__import__(debugger).set_trace()
else:
@@ -2214,8 +2400,52 @@ class IrQWeb(models.AbstractModel):
else:
return self._generate_asset_nodes_cache(bundle, css, js, debug, async_load, defer_load, lazy_load, media)
# other methods used for the asset bundles
# qweb cache feature
def _get_cache_key(self, cache_key):
"""
Convert the template cache key item into a hashable key.
:param cache_key: tuple
:returns: tuple of hashable items
"""
if not isinstance(cache_key, (tuple, list)):
cache_key = (cache_key,)
keys = []
for item in cache_key:
try:
# use try catch instead of isinstance to detect lazy values
keys.append(item._name)
keys.append(tuple(item.ids))
dates = item.mapped('write_date')
if dates:
keys.append(max(dates).timestamp())
except AttributeError:
keys.append(repr(item))
return tuple(keys)
def _load_values(self, cache_key, get_value, loaded_values=None):
""" generate value from the function if the result is not cached. """
if not cache_key:
return get_value()
value = loaded_values and loaded_values.get(cache_key)
if not value:
value = self._get_cached_values(cache_key, get_value)
if loaded_values is not None:
loaded_values[cache_key] = value
return value
# The cache does not need to be invalidated if the 'base_key_cache'
# in '_compile' method contains the write_date of all inherited views.
@tools.conditional(
'xml' not in tools.config['dev_mode'],
tools.ormcache('cache_key'),
)
def _get_cached_values(self, cache_key, get_value):
""" generate value from the function if the result is not cached. """
return get_value()
# other methods used for the asset bundles
@tools.conditional(
# in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
# by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
@@ -2305,10 +2535,20 @@ def render(template_name, values, load, **options):
pool = MockPool()
def _load(self, ref):
"""
Load the template referenced by ``ref``.
:returns: The loaded template (as string or etree) and its
identifier
:rtype: Tuple[Union[etree, str], Optional[str, int]]
"""
return self.env.context['load'](ref)
def _prepare_environment(self, values):
values['true'] = True
values['false'] = False
return self
return self.with_context(is_t_cache_disabled=True, __qweb_loaded_values={})
def _get_field(self, *args):
raise NotImplementedError("Fields are not allowed in this rendering mode. Please use \"env['ir.qweb']._render\" method")
+24 -24
View File
@@ -464,15 +464,15 @@ class TestProfiling(TransactionCase):
'key': 'root',
'arch_db': '''<t t-name="root">
<t t-foreach="{'a': 3, 'b': 2, 'c': 1}" t-as="item">
[<t t-esc="item_index"/>: <t t-call="base.dummy" t-set-record="item"/> <t t-esc="item_value"/>]
<b t-esc="add_one_query()"/></t>
[<t t-out="item_index"/>: <t t-set="record" t-value="item"/><t t-call="base.dummy"/> <t t-out="item_value"/>]
<b t-out="add_one_query()"/></t>
</t>'''
})
child_template = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
'key': 'dummy',
'arch_db': '<t t-name="dummy"><span t-attf-class="myclass"><t t-esc="record"/> <t t-esc="add_one_query()"/></span></t>'
'arch_db': '<t t-name="dummy"><span t-attf-class="myclass"><t t-out="record"/> <t t-out="add_one_query()"/></span></t>'
})
self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)"
"VALUES ('dummy', 'ir.ui.view', %s, 'base')", [child_template.id])
@@ -518,33 +518,33 @@ class TestProfiling(TransactionCase):
# first template and first directive
{'view_id': template.id, 'xpath': '/t/t', 'directive': """t-foreach="{'a': 3, 'b': 2, 'c': 1}" t-as='item'""", 'query': 0},
# first pass in the loop
{'view_id': template.id, 'xpath': '/t/t/t[1]', 'directive': "t-esc='item_index'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-call='base.dummy'", 'query': 0}, # 0 because the template is in ir.ui.view cache
{'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-set-record='item'", 'query': 0}, # t-set it's consumed by t-call
{'view_id': template.id, 'xpath': '/t/t/t[1]', 'directive': "t-out='item_index'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-set='record' t-value='item'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/t[3]', 'directive': "t-call='base.dummy'", 'query': 0}, # 0 because the template is in ir.ui.view cache
# first pass in the loop: content of the child template
{'view_id': child_template.id, 'xpath': '/t/span', 'directive': "t-attf-class='myclass'", 'query': 0},
{'view_id': child_template.id, 'xpath': '/t/span/t[1]', 'directive': "t-esc='record'", 'query': 0},
{'view_id': child_template.id, 'xpath': '/t/span/t[2]', 'directive': "t-esc='add_one_query()'", 'query': 1},
{'view_id': template.id, 'xpath': '/t/t/t[3]', 'directive': "t-esc='item_value'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/b', 'directive': "t-esc='add_one_query()'", 'query':1},
{'view_id': child_template.id, 'xpath': '/t/span/t[1]', 'directive': "t-out='record'", 'query': 0},
{'view_id': child_template.id, 'xpath': '/t/span/t[2]', 'directive': "t-out='add_one_query()'", 'query': 1},
{'view_id': template.id, 'xpath': '/t/t/t[4]', 'directive': "t-out='item_value'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/b', 'directive': "t-out='add_one_query()'", 'query':1},
# second pass in the loop
{'view_id': template.id, 'xpath': '/t/t/t[1]', 'directive': "t-esc='item_index'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-call='base.dummy'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-set-record='item'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/t[1]', 'directive': "t-out='item_index'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-set='record' t-value='item'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/t[3]', 'directive': "t-call='base.dummy'", 'query': 0},
{'view_id': child_template.id, 'xpath': '/t/span', 'directive': "t-attf-class='myclass'", 'query': 0},
{'view_id': child_template.id, 'xpath': '/t/span/t[1]', 'directive': "t-esc='record'", 'query': 0},
{'view_id': child_template.id, 'xpath': '/t/span/t[2]', 'directive': "t-esc='add_one_query()'", 'query': 1},
{'view_id': template.id, 'xpath': '/t/t/t[3]', 'directive': "t-esc='item_value'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/b', 'directive': "t-esc='add_one_query()'", 'query':1},
{'view_id': child_template.id, 'xpath': '/t/span/t[1]', 'directive': "t-out='record'", 'query': 0},
{'view_id': child_template.id, 'xpath': '/t/span/t[2]', 'directive': "t-out='add_one_query()'", 'query': 1},
{'view_id': template.id, 'xpath': '/t/t/t[4]', 'directive': "t-out='item_value'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/b', 'directive': "t-out='add_one_query()'", 'query':1},
# third pass in the loop
{'view_id': template.id, 'xpath': '/t/t/t[1]', 'directive': "t-esc='item_index'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-call='base.dummy'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-set-record='item'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/t[1]', 'directive': "t-out='item_index'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-set='record' t-value='item'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/t[3]', 'directive': "t-call='base.dummy'", 'query': 0},
{'view_id': child_template.id, 'xpath': '/t/span', 'directive': "t-attf-class='myclass'", 'query': 0},
{'view_id': child_template.id, 'xpath': '/t/span/t[1]', 'directive': "t-esc='record'", 'query': 0},
{'view_id': child_template.id, 'xpath': '/t/span/t[2]', 'directive': "t-esc='add_one_query()'", 'query': 1},
{'view_id': template.id, 'xpath': '/t/t/t[3]', 'directive': "t-esc='item_value'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/b', 'directive': "t-esc='add_one_query()'", 'query':1},
{'view_id': child_template.id, 'xpath': '/t/span/t[1]', 'directive': "t-out='record'", 'query': 0},
{'view_id': child_template.id, 'xpath': '/t/span/t[2]', 'directive': "t-out='add_one_query()'", 'query': 1},
{'view_id': template.id, 'xpath': '/t/t/t[4]', 'directive': "t-out='item_value'", 'query': 0},
{'view_id': template.id, 'xpath': '/t/t/b', 'directive': "t-out='add_one_query()'", 'query':1},
]
self.assertEqual(data, expected)
File diff suppressed because it is too large Load Diff
-5
View File
@@ -130,8 +130,3 @@ class Warning(UserError):
def __init__(self, *args, **kwargs):
warnings.warn("Warning is a deprecated alias to UserError.", DeprecationWarning)
super().__init__(*args, **kwargs)
class QWebException(Exception):
def __init__(self, *args, **kwargs):
warnings.warn("qweb.QWebException is the exception you are looking for.", DeprecationWarning)
super().__init__(*args, **kwargs)
+1 -1
View File
@@ -9,6 +9,7 @@ the ORM does, in fact.
"""
from contextlib import contextmanager
from datetime import timedelta, datetime
import itertools
import logging
import os
@@ -17,7 +18,6 @@ import threading
import uuid
import warnings
from datetime import timedelta, datetime
from inspect import currentframe
import psycopg2
import psycopg2.extras
+13 -8
View File
@@ -296,17 +296,22 @@ class QwebTracker():
if not self.env.context.get('profile'):
return method_compile(self, template)
render_template = method_compile(self, template)
def profiled_method_compile(self, values, gen0=None):
ref = render_template.options.get('ref')
ref_xml = render_template.options.get('ref_xml')
template_functions, def_name = method_compile(self, template)
render_template = template_functions[def_name]
def profiled_method_compile(self, values):
options = template_functions['options']
ref = options.get('ref')
ref_xml = options.get('ref_xml')
qweb_tracker = QwebTracker(ref, ref_xml, self.env.cr)
self = self.with_context(qweb_tracker=qweb_tracker)
if qweb_tracker.execution_context_enabled:
with ExecutionContext(template=ref):
return render_template(self, values, gen0)
return render_template(self, values, gen0)
return profiled_method_compile
return render_template(self, values)
return render_template(self, values)
template_functions[def_name] = profiled_method_compile
return (template_functions, def_name)
return _tracked_compile
@classmethod
@@ -315,7 +320,7 @@ class QwebTracker():
def _tracked_compile_directive(self, el, options, directive, level):
if not options.get('profile') or directive in ('inner-content', 'tag-open', 'tag-close'):
return method_compile_directive(self, el, options, directive, level)
enter = f"{' ' * 4 * level}self.env.context['qweb_tracker'].enter_directive({directive!r}, {el.attrib!r}, {options['last_path_node']!r})"
enter = f"{' ' * 4 * level}self.env.context['qweb_tracker'].enter_directive({directive!r}, {el.attrib!r}, {options['_qweb_error_path_xml'][0]!r})"
leave = f"{' ' * 4 * level}self.env.context['qweb_tracker'].leave_directive()"
code_directive = method_compile_directive(self, el, options, directive, level)
return [enter, *code_directive, leave] if code_directive else []