diff --git a/CHANGES.rst b/CHANGES.rst index eb46957df..dd358ac54 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,8 @@ Unreleased - Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``. :pr:`1793` - Use ``flit_core`` instead of ``setuptools`` as build backend. +- Add function decorator ``@render_time_only`` for filters and tests. + :issue:`1752` Version 3.1.3 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. diff --git a/src/jinja2/nodes.py b/src/jinja2/nodes.py index 00365ed83..6c0f2b7fc 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 hasattr(func, "jinja2_render_time_only"): + 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 4b4720f6d..8018f4a0e 100644 --- a/src/jinja2/utils.py +++ b/src/jinja2/utils.py @@ -86,6 +86,18 @@ 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. + + .. versionadded:: 3.2.0 + """ + f.jinja2_render_time_only = True # type: ignore + return f + + def internalcode(f: F) -> F: """Marks the function as internally used""" internal_code.add(f.__code__) 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):