From 522de067b02fd579413be4fc39c5021614d127e6 Mon Sep 17 00:00:00 2001 From: Louis Duret-Robert Date: Fri, 11 Nov 2022 16:49:35 +0100 Subject: [PATCH 1/5] New decorator to force filters to be resolved at render time and not compile-time constants --- src/jinja2/nodes.py | 4 ++++ src/jinja2/utils.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/jinja2/nodes.py b/src/jinja2/nodes.py index b2f88d9d9..d308632eb 100644 --- a/src/jinja2/nodes.py +++ b/src/jinja2/nodes.py @@ -759,6 +759,10 @@ def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any: func = env_map.get(self.name) pass_arg = _PassArg.from_obj(func) # type: ignore + # Don't resolve functions decorated as render-time only + if getattr(func, "jinja2_render_time_only", False): + raise Impossible() + if func is None or pass_arg is _PassArg.context: raise Impossible() diff --git a/src/jinja2/utils.py b/src/jinja2/utils.py index 9b5f5a50e..b869600f9 100644 --- a/src/jinja2/utils.py +++ b/src/jinja2/utils.py @@ -86,6 +86,16 @@ def from_obj(cls, obj: F) -> t.Optional["_PassArg"]: return None +def render_time_only(f: F) -> F: + """Never resolve the function as a constant during compilation, and + always leave it for rendering phase. + + Can be used on filters and tests. + """ + f.jinja2_render_time_only = True + return f + + def internalcode(f: F) -> F: """Marks the function as internally used""" internal_code.add(f.__code__) From 52fbff3d57d95a96ee6e65d87ff217721ad8b1ab Mon Sep 17 00:00:00 2001 From: Louis Duret-Robert Date: Fri, 11 Nov 2022 17:29:37 +0100 Subject: [PATCH 2/5] Test for the render_time_only decorator --- tests/test_regression.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_regression.py b/tests/test_regression.py index 46e492bdd..9209f56da 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -8,6 +8,7 @@ from jinja2 import TemplateNotFound from jinja2 import TemplateSyntaxError from jinja2.utils import pass_context +from jinja2.utils import render_time_only class TestCorner: @@ -736,6 +737,31 @@ def test_nested_loop_scoping(self, env): ) assert tmpl.render() == "hellohellohello" + def test_decorator_render_time_only_filter(self, env): + # Filter not decorated, should be resolved to constant during compilation + def filter_compile_time_const(content): + return "filter_compile_time_const_return" + + env.filters["filter_compile_time_const"] = filter_compile_time_const + + # Filter decorated, should not be resolved during compilation + @render_time_only + def filter_render_time_only(content): + return "filter_render_time_only_return" + + env.filters["filter_render_time_only"] = filter_render_time_only + + # Template to just call the two filters + tmpl = "{{0|filter_compile_time_const}}{{0|filter_render_time_only}}" + + # Get the raw compiled template before rendering + tmpl_compile = env.compile(tmpl, raw=True) + + # If filter was resolved during compilation, it generated a yield of + # its return value + assert "yield 'filter_compile_time_const_return'" in tmpl_compile + assert "yield 'filter_render_time_only_return'" not in tmpl_compile + @pytest.mark.parametrize("unicode_char", ["\N{FORM FEED}", "\x85"]) def test_unicode_whitespace(env, unicode_char): From d235ac3a23e3947f8d9540ca5f856706067dad9d Mon Sep 17 00:00:00 2001 From: Louis Duret-Robert Date: Fri, 11 Nov 2022 17:47:41 +0100 Subject: [PATCH 3/5] Document the render_time_only decorator --- docs/api.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index e2c9bd526..956c14462 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -672,6 +672,15 @@ Now it can be used in templates: {{ article.pub_date|datetimeformat }} {{ article.pub_date|datetimeformat("%B %Y") }} +If the call of a filter can be computed at compile time, then it will be +resolved as a constant before rendering. For example, a filter called on the +iterated variable of a ``for`` loop cannot be compiled to a constant since +its argument is only resolved at render time ; but the function of a filter +called on a constant in an ``if`` statement will be executed at compile time +even if the condition of the ``if`` statement is not met. A filter can be +prevented to be compiled to a constant by decorating the function with the +``render_time_only`` decorator. + Some decorators are available to tell Jinja to pass extra information to the filter. The object is passed as the first argument, making the value being filtered the second argument. From 47b0142e2b39c4814d8c583d64f091d49561456d Mon Sep 17 00:00:00 2001 From: Louis Duret-Robert Date: Fri, 11 Nov 2022 20:48:58 +0100 Subject: [PATCH 4/5] Small change of render_time_only for tox --- src/jinja2/nodes.py | 2 +- src/jinja2/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jinja2/nodes.py b/src/jinja2/nodes.py index d308632eb..9cfd35400 100644 --- a/src/jinja2/nodes.py +++ b/src/jinja2/nodes.py @@ -760,7 +760,7 @@ def as_const(self, eval_ctx: t.Optional[EvalContext] = None) -> t.Any: pass_arg = _PassArg.from_obj(func) # type: ignore # Don't resolve functions decorated as render-time only - if getattr(func, "jinja2_render_time_only", False): + if hasattr(func, "jinja2_render_time_only"): raise Impossible() if func is None or pass_arg is _PassArg.context: diff --git a/src/jinja2/utils.py b/src/jinja2/utils.py index b869600f9..e25f4dded 100644 --- a/src/jinja2/utils.py +++ b/src/jinja2/utils.py @@ -92,7 +92,7 @@ def render_time_only(f: F) -> F: Can be used on filters and tests. """ - f.jinja2_render_time_only = True + f.jinja2_render_time_only = True # type: ignore return f From 33f51dd3635d9811f96e67503a02a1a3904d87a0 Mon Sep 17 00:00:00 2001 From: Louis Duret-Robert Date: Fri, 11 Nov 2022 21:09:32 +0100 Subject: [PATCH 5/5] Add render_time_only to changelog --- CHANGES.rst | 3 +++ src/jinja2/utils.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7ee75a6a7..46f6d1285 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Version 3.2.0 Unreleased +- Add function decorator ``@render_time_only`` for filters and tests. + :issue:`1752` + Version 3.1.2 ------------- diff --git a/src/jinja2/utils.py b/src/jinja2/utils.py index e25f4dded..db1a44a4d 100644 --- a/src/jinja2/utils.py +++ b/src/jinja2/utils.py @@ -91,6 +91,8 @@ def render_time_only(f: F) -> F: always leave it for rendering phase. Can be used on filters and tests. + + .. versionadded:: 3.2.0 """ f.jinja2_render_time_only = True # type: ignore return f