Skip to content
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
1 change: 1 addition & 0 deletions changelog/69821.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Files generated by a Jinja template no longer get a newline too many at the end when the template ends with a Jinja tag.
16 changes: 5 additions & 11 deletions salt/utils/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,17 +369,11 @@ def render_jinja_tmpl(tmplstr, context, tmplpath=None):
opts = opts.value()
saltenv = context["saltenv"]
loader = None
newline = False

if tmplstr and not isinstance(tmplstr, str):
# https://jinja.palletsprojects.com/en/2.11.x/api/#unicode
tmplstr = tmplstr.decode(SLS_ENCODING)

if tmplstr.endswith(os.linesep):
newline = os.linesep
elif tmplstr.endswith("\n"):
newline = "\n"

try:
if not saltenv:
if tmplpath:
Expand Down Expand Up @@ -412,6 +406,11 @@ def render_jinja_tmpl(tmplstr, context, tmplpath=None):
opt_jinja_sls_env if isinstance(opt_jinja_sls_env, dict) else {}
)

if "keep_trailing_newline" not in opt_jinja_env:
opt_jinja_env["keep_trailing_newline"] = True
if "keep_trailing_newline" not in opt_jinja_sls_env:
opt_jinja_sls_env["keep_trailing_newline"] = True

# Pass through trim_blocks and lstrip_blocks Jinja parameters
# trim_blocks removes newlines around Jinja blocks
# lstrip_blocks strips tabs and spaces from the beginning of
Expand Down Expand Up @@ -544,11 +543,6 @@ def opt_jinja_env_helper(opts, optname):
if loader and isinstance(loader, salt.utils.jinja.SaltCacheLoader):
loader.destroy()

# Workaround a bug in Jinja that removes the final newline
# (https://github.com/mitsuhiko/jinja2/issues/75)
if newline:
output += newline

return output


Expand Down
10 changes: 10 additions & 0 deletions tests/pytests/unit/utils/jinja/test_jinja_default_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,13 @@ def test_statement_prefix(minion_opts, local_salt):
template, dict(opts=minion_opts, saltenv="test", salt=local_salt)
)
assert rendered == "onetwothree"

def test_no_extra_trailing_newline(minion_opts, local_salt):
template = """{% for item in ['one', 'two', 'three'] -%}
{{ item }}
{% endfor -%}
"""
rendered = render_jinja_tmpl(
template, dict(opts=minion_opts, saltenv="test", salt=local_salt)
)
assert rendered == "one\ntwo\nthree\n"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might need to use os.linesep instead of \n. We'll see if the tests pass on Windows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please test a few more scenarios here:

Test Single-Line Template Without Trailing Newline

def test_no_trailing_newline_input(minion_opts, local_salt):
    # String explicitly does NOT end with a newline
    template = "{% for item in ['one', 'two', 'three'] -%}\n{{ item }}\n{% endfor -%}"
    rendered = render_jinja_tmpl(
        template, dict(opts=minion_opts, saltenv="test", salt=local_salt)
    )
    # Expect output without a trailing newline
    assert rendered == "one\ntwo\nthree"

Test Explicit User Override (keep_trailing_newline = False)

def test_explicit_keep_trailing_newline_false(minion_opts, local_salt):
    opts = minion_opts.copy()
    opts["renderer_options"] = {"jinja_env": {"keep_trailing_newline": False}}

    template = "hello\n"
    rendered = render_jinja_tmpl(
        template, dict(opts=opts, saltenv="test", salt=local_salt)
    )
    # With keep_trailing_newline=False explicitly set, Jinja strips the newline
    assert rendered == "hello"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result of test_no_trailing_newline_input will be "one\ntwo\nthree\n" since there's a newline in the body of the for loop, and both the original code and the Jinja option keep_trailing_newline only look at trailing newlines in the template. They don't strip trailing newlines from the output.

So:

  • template: "{% for item in ['one', 'two', 'three'] -%}\n{{ item }}\n{% endfor -%}\n"
    keep_trailing_newline: doesn't matter
    result before: "one\ntwo\nthree\n\n"
    result after: "one\ntwo\nthree\n"
  • template: "{% for item in ['one', 'two', 'three'] -%}\n{{ item }}\n{% endfor -%}"
    keep_trailing_newline: doesn't matter
    result unchanged: "one\ntwo\nthree\n"
  • template: "{% for item in ['one', 'two', 'three'] -%}\n{{ item }}\n{% endfor %}\n"
    keep_trailing_newline: False
    result unchanged: "one\ntwo\nthree\n\n"
  • template: "{% for item in ['one', 'two', 'three'] -%}\n{{ item }}\n{% endfor %}\n"
    keep_trailing_newline: True
    result before: "one\ntwo\nthree\n\n\n"
    result after: "one\ntwo\nthree\n\n"
  • template: "{% for item in ['one', 'two', 'three'] -%}\n{{ item }}\n{% endfor %}"
    keep_trailing_newline: doesn't matter
    result unchanged: "one\ntwo\nthree\n"
  • template: "hello\n"
    keep_trailing_newline: False
    result before: "hello\n"
    result after: "hello"
  • template: "hello\n"
    keep_trailing_newline: True
    result before: "hello\n\n"
    result after: "hello\n"
  • template: "hello"
    keep_trailing_newline: doesn't matter
    result unchanged: "hello"

I will add a few more tests.