From d0c0299dc8bf5304b1ff642215558023d8bf5c78 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 | 1 + src/jinja2/compiler.py | 2 +- src/jinja2/idtracking.py | 2 +- tests/test_compile.py | 118 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 tests/test_compile.py diff --git a/CHANGES.rst b/CHANGES.rst index fc540af5a..abb37b961 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,7 @@ Version 3.0.2 Unreleased +- Make ``compile_templates`` deterministic. :issue:`1452, 1453` Version 3.0.1 ------------- diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py index ef4c0a1f1..98d69ae9a 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..93ac0018c --- /dev/null +++ b/tests/test_compile.py @@ -0,0 +1,118 @@ +import os +import shutil +import tempfile + +from jinja2 import Environment +from jinja2 import loaders + + +class TestCompileTemplate: + archive = None + + def setup(self): + self.archive = tempfile.mkdtemp() + + def teardown(self): + shutil.rmtree(self.archive) + + def test_filters(self): + env = Environment( + loader=loaders.DictLoader( + { + "foo": """ +{{ 1|filter1 }} +{{ 2|filter2 }} +""" + } + ) + ) + + env.filters["filter1"] = lambda value: value + env.filters["filter2"] = lambda value: value + + env.compile_templates(self.archive, zip=None, ignore_errors=False) + + files = os.listdir(self.archive) + assert len(files) == 1 + + with open(os.path.join(self.archive, files[0])) as f: + assert f.read().splitlines() == [ + "from __future__ import generator_stop", + "from jinja2.runtime import LoopContext, Macro, Markup," + " Namespace, TemplateNotFound, TemplateReference," + " TemplateRuntimeError, Undefined, concat, escape, identity," + " internalcode, markup_join, missing, str_join", + "name = 'foo'", + "", + "def root(context, missing=missing):", + " resolve = context.resolve_or_missing", + " undefined = environment.undefined", + " cond_expr_undefined = Undefined", + " if 0: yield None", + " try:", + " t_1 = environment.filters['filter1']", + " except KeyError:", + " @internalcode", + " def t_1(*unused):", + ' raise TemplateRuntimeError("No filter named' + " 'filter1' found.\")", + " try:", + " t_2 = environment.filters['filter2']", + " except KeyError:", + " @internalcode", + " def t_2(*unused):", + ' raise TemplateRuntimeError("No filter named' + " 'filter2' found.\")", + " pass", + " yield '\\n1\\n2'", + "", + "blocks = {}", + "debug_info = ''", + ] + + def test_import_as_with_context(self): + env = Environment( + loader=loaders.DictLoader( + { + "foo": """ +{% import "bar" as bar with context %} +{% import "baz" as baz with context %} +""" + } + ) + ) + env.compile_templates(self.archive, zip=None, ignore_errors=False) + + files = os.listdir(self.archive) + assert len(files) == 1 + + with open(os.path.join(self.archive, files[0])) as f: + assert f.read().splitlines() == [ + "from __future__ import generator_stop", + "from jinja2.runtime import LoopContext, Macro, Markup," + " Namespace, TemplateNotFound, TemplateReference," + " TemplateRuntimeError, Undefined, concat, escape, identity," + " internalcode, markup_join, missing, str_join", + "name = 'foo'", + "", + "def root(context, missing=missing):", + " resolve = context.resolve_or_missing", + " undefined = environment.undefined", + " cond_expr_undefined = Undefined", + " if 0: yield None", + " l_0_bar = l_0_baz = missing", + " pass", + " yield '\\n'", + " l_0_bar = context.vars['bar'] = environment.get_template(" + "'bar', 'foo').make_module(context.get_all(), True, {'bar':" + " l_0_bar, 'baz': l_0_baz})", + " context.exported_vars.discard('bar')", + " yield '\\n'", + " l_0_baz = context.vars['baz'] = environment.get_template(" + "'baz', 'foo').make_module(context.get_all(), True, {'bar':" + " l_0_bar, 'baz': l_0_baz})", + " context.exported_vars.discard('baz')", + "", + "blocks = {}", + "debug_info = '2=13&3=16'", + ]