Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render time only filter decorator #1759

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/jinja2/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
12 changes: 12 additions & 0 deletions src/jinja2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
26 changes: 26 additions & 0 deletions tests/test_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
Loading