Skip to content
New issue

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

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

Already on GitHub? # to your account

Content of if-else block is computed even if condition is false #1752

Closed
Louis-DR opened this issue Nov 4, 2022 · 3 comments
Closed

Content of if-else block is computed even if condition is false #1752

Louis-DR opened this issue Nov 4, 2022 · 3 comments

Comments

@Louis-DR
Copy link

Louis-DR commented Nov 4, 2022

Hello,

I've noticed filters inside an if-else loop are triggered regardless of the condition result. However I have two issues with this :

  1. We spend resources and performance rendering all blocks, which in certain cases can increate the total rendering time by a lot.
  2. When using extensions or filters that don't just return text, they are executed regardless.

For instance, when using the following custom filters

import time

def log(txt):
  print(txt)
  return txt

def sleep(txt):
  time.sleep(5)
  return txt

And this template

{% set condition = true %}

{% if condition %}
{{ "Hello" | log   }}
{{ ""      | sleep }}
{% else %}
{{ "World" | log   }}
{{ ""      | sleep }}
{% endif %}

The resulting rendered file will only contain Hello, but rendering will take 10 seconds (instead of only 5), and the console will print Hello and World.

In my case I use custom filters to generate warnings or errors when checking the configuration in the template, and also I have some special filters that write/append to other files and change environment settings/variables.

I think this could also cause issues with the do expression.

Is this expected behaviour or perhaps something I configured incorrectly ?

Python version 3.10.7
Jinja2 version 3.1.2

@Louis-DR
Copy link
Author

Louis-DR commented Nov 4, 2022

I've investigated this issue more, and I have to admit I don't understand how Jinja2 parses a template. I've tried disabling optimization (optimized=False).

With the following template :

{% set condition = true %}

{% set ns = namespace(foo="✔one") %}
{{ ("✔1 Should see this at the start : "~ns.foo) | log }}
{{  "✔2 Should see this" | log }}
{{ ns.foo | log }}

{% if condition %}
  {{ "✔3 Should see this" | log }}
  {% set ns.foo = "✔two" | log %}
{% else %}
  {{ "✘1 Should not see this !" | log }}
  {% set ns.foo = "✘three" | log %}
{% endif %}

{% if not condition %}
  {% set ns.foo = "✘four" | log %}
  {{ ns.foo | log }}
  {{ "✘2 Should not see this either !" | log }}
{% endif %}

{{ ("✔4 Should see this : "~ns.foo) | log }}
{{  "✔5 Should see this at the end" | log }}

With optimization, the console prints :

✔2 Should see this
✔3 Should see this
✔two
✘1 Should not see this !
✘three
✘four
✘2 Should not see this either !
✔5 Should see this at the end
✔1 Should see this at the start : ✔one
✔one
✔4 Should see this : ✔two

Without optimization, the console prints :

✔2 Should see this
✔3 Should see this
✘1 Should not see this !
✘2 Should not see this either !
✔5 Should see this at the end
✔1 Should see this at the start : ✔one
✔one
✔two
✔4 Should see this : ✔two

In either cases we see things we shouldn't, and the order is all wrong.

Note that logging here is only to demonstrate which chunks are rendered and which are not. The two big issues are performance, and special filters that should not be executed.

@davidism
Copy link
Member

davidism commented Nov 4, 2022

When Jinja compiles templates, it attempts to evaluate expressions to see if they are constant at compile time. This "evaluate constant" optimization is separate from the env.optimized flag, which does "constant folding" on expressions, which can result in different compilation/runtime behavior. At compile time, Jinja processes the entire AST, then generates Python code; things like an if block only have an effect at runtime.

The general advice for templates (not only Jinja, in general) is to avoid side effects and perform data processing in the Python code before rendering. Templates are intended for rendering only.

You can play around with your code more by using env.compile(source, raw=True) to see the Python code that will be evaluated at runtime, as well as calling breakpoint() in your filter function to see the stack during compilation and runtime.

env = Environment(optimized=True)

def log(value):
    breakpoint()
    print(value)
    return value.upper()

env.filters["log"] = log

source = """\
{% set condition = true %}
{{ "start" | log }}
{% if condition %}
{{ "a" | log }}
{% else %}
{{ "b" | log }}
{% endif %}
"""
print(env.compile(source, raw=True))  # see compile time
# print(env.from_string(source).render())  # see runtime

@davidism davidism closed this as completed Nov 4, 2022
@Louis-DR
Copy link
Author

Thanks for the details. Looking over at the compiled code it makes sense.
I still believe it would be very useful to have a way to only execute some statements (filters, includes and macros with tests etc) when a condition is met. I understand that Jinja2 is not made for the logic part, but for performance reasons this is important, for instance if someone has a filter that makes a request for some data, or that parses the data in a complex and resource intensive way.

Perhaps the addition of a conditional-loop context similar to the for-loop context ? This context would contain at least a simple bool attribute to pass to a filter/test to implement the logic in the python function. Perhaps a decorator on the filter and test functions could also be added ?

If you think that is relevant can we turn this ticket into a feature request ? I could start looking into the source code and try to add the loop context.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jan 10, 2023
# for free to subscribe to this conversation on GitHub. Already have an account? #.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants