From 4c703ec44dc74b25ba4f1fb7b87bf9106313ffcf Mon Sep 17 00:00:00 2001 From: Takuto Ikuta Date: Wed, 26 May 2021 17:09:33 +0900 Subject: [PATCH] make compile_templates deterministic Python3 doesn't keep insertion order for set(), so this sorts some places for deterministic output for compiled template. --- CHANGES.rst | 2 ++ src/jinja2/compiler.py | 2 +- src/jinja2/idtracking.py | 2 +- tests/test_compile.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 tests/test_compile.py diff --git a/CHANGES.rst b/CHANGES.rst index 7bfebffcc..116461af2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,8 @@ Unreleased - Fix a loop scoping bug that caused assignments in nested loops to still be referenced outside of it. :issue:`1427` +- Make ``compile_templates`` deterministic for filter and import + names. :issue:`1452, 1453` Version 3.0.1 diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py index 5fe934949..b6297202a 100644 --- a/src/jinja2/compiler.py +++ b/src/jinja2/compiler.py @@ -556,7 +556,7 @@ def pull_dependencies(self, nodes: t.Iterable[nodes.Node]) -> None: visitor.tests, "tests", ): - for name in names: + for name in sorted(names): if name not in id_map: id_map[name] = self.temporary_identifier() diff --git a/src/jinja2/idtracking.py b/src/jinja2/idtracking.py index a2e7a055e..38c525ee3 100644 --- a/src/jinja2/idtracking.py +++ b/src/jinja2/idtracking.py @@ -149,7 +149,7 @@ def dump_stores(self) -> t.Dict[str, str]: node: t.Optional["Symbols"] = self while node is not None: - for name in node.stores: + for name in sorted(node.stores): if name not in rv: rv[name] = self.find_ref(name) # type: ignore diff --git a/tests/test_compile.py b/tests/test_compile.py new file mode 100644 index 000000000..42a773f21 --- /dev/null +++ b/tests/test_compile.py @@ -0,0 +1,28 @@ +import os +import re + +from jinja2.environment import Environment +from jinja2.loaders import DictLoader + + +def test_filters_deterministic(tmp_path): + src = "".join(f"{{{{ {i}|filter{i} }}}}" for i in range(10)) + env = Environment(loader=DictLoader({"foo": src})) + env.filters.update(dict.fromkeys((f"filter{i}" for i in range(10)), lambda: None)) + env.compile_templates(tmp_path, zip=None) + name = os.listdir(tmp_path)[0] + content = (tmp_path / name).read_text("utf8") + expect = [f"filters['filter{i}']" for i in range(10)] + found = re.findall(r"filters\['filter\d']", content) + assert found == expect + + +def test_import_as_with_context_deterministic(tmp_path): + src = "\n".join(f'{{% import "bar" as bar{i} with context %}}' for i in range(10)) + env = Environment(loader=DictLoader({"foo": src})) + env.compile_templates(tmp_path, zip=None) + name = os.listdir(tmp_path)[0] + content = (tmp_path / name).read_text("utf8") + expect = [f"'bar{i}': " for i in range(10)] + found = re.findall(r"'bar\d': ", content)[:10] + assert found == expect