diff options
-rw-r--r-- | CHANGES.rst | 7 | ||||
-rw-r--r-- | CONTRIBUTING.rst | 10 | ||||
-rw-r--r-- | README.rst | 2 | ||||
-rw-r--r-- | src/jinja2/compiler.py | 7 | ||||
-rw-r--r-- | src/jinja2/idtracking.py | 2 | ||||
-rw-r--r-- | src/jinja2/runtime.py | 3 | ||||
-rw-r--r-- | tests/test_api.py | 3 | ||||
-rw-r--r-- | tests/test_compile.py | 28 | ||||
-rw-r--r-- | tests/test_regression.py | 7 |
9 files changed, 61 insertions, 8 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index a343496..f321784 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,7 +9,12 @@ Unreleased Version 3.0.2 ------------- -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` +- Revert an unintended change that caused ``Undefined`` to act like + ``StrictUndefined`` for the ``in`` operator. :issue:`1448` Version 3.0.1 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 553e6a3..5f83503 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -92,7 +92,7 @@ First time setup .. code-block:: text - git remote add fork https://github.com/{username}/jinja + $ git remote add fork https://github.com/{username}/jinja - Create a virtualenv. @@ -107,6 +107,12 @@ First time setup > env\Scripts\activate +- Upgrade pip and setuptools. + + .. code-block:: text + + $ python -m pip install --upgrade pip setuptools + - Install the development dependencies, then install Jinja in editable mode. @@ -138,7 +144,7 @@ Start coding .. code-block:: text $ git fetch origin - $ git checkout -b your-branch-name origin/1.1.x + $ git checkout -b your-branch-name origin/3.0.x If you're submitting a feature addition or change, branch off of the "main" branch. @@ -35,7 +35,7 @@ Install and update using `pip`_: $ pip install -U Jinja2 -.. _pip: https://pip.pypa.io/en/stable/quickstart/ +.. _pip: https://pip.pypa.io/en/stable/getting-started/ In A Nutshell diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py index ef4c0a1..b629720 100644 --- a/src/jinja2/compiler.py +++ b/src/jinja2/compiler.py @@ -556,7 +556,7 @@ class CodeGenerator(NodeVisitor): visitor.tests, "tests", ): - for name in names: + for name in sorted(names): if name not in id_map: id_map[name] = self.temporary_identifier() @@ -1290,6 +1290,11 @@ class CodeGenerator(NodeVisitor): self.write(", loop)") self.end_write(frame) + # at the end of the iteration, clear any assignments made in the + # loop from the top level + if self._assign_stack: + self._assign_stack[-1].difference_update(loop_frame.symbols.stores) + def visit_If(self, node: nodes.If, frame: Frame) -> None: if_frame = frame.soft() self.writeline("if ", node) diff --git a/src/jinja2/idtracking.py b/src/jinja2/idtracking.py index a2e7a05..38c525e 100644 --- a/src/jinja2/idtracking.py +++ b/src/jinja2/idtracking.py @@ -149,7 +149,7 @@ class Symbols: 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/src/jinja2/runtime.py b/src/jinja2/runtime.py index 87bb132..2346cf2 100644 --- a/src/jinja2/runtime.py +++ b/src/jinja2/runtime.py @@ -915,7 +915,7 @@ class Undefined: __floordiv__ = __rfloordiv__ = _fail_with_undefined_error __mod__ = __rmod__ = _fail_with_undefined_error __pos__ = __neg__ = _fail_with_undefined_error - __call__ = __getitem__ = __contains__ = _fail_with_undefined_error + __call__ = __getitem__ = _fail_with_undefined_error __lt__ = __le__ = __gt__ = __ge__ = _fail_with_undefined_error __int__ = __float__ = __complex__ = _fail_with_undefined_error __pow__ = __rpow__ = _fail_with_undefined_error @@ -1091,6 +1091,7 @@ class StrictUndefined(Undefined): __slots__ = () __iter__ = __str__ = __len__ = Undefined._fail_with_undefined_error __eq__ = __ne__ = __bool__ = __hash__ = Undefined._fail_with_undefined_error + __contains__ = Undefined._fail_with_undefined_error # Remove slots attributes, after the metaclass is applied they are diff --git a/tests/test_api.py b/tests/test_api.py index 774bb3c..4db3b4a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -316,7 +316,7 @@ class TestUndefined: assert env.from_string("{{ foo.missing }}").render(foo=42) == "" assert env.from_string("{{ not missing }}").render() == "True" pytest.raises(UndefinedError, env.from_string("{{ missing - 1}}").render) - pytest.raises(UndefinedError, env.from_string("{{ 'foo' in missing }}").render) + assert env.from_string("{{ 'foo' in missing }}").render() == "False" und1 = Undefined(name="x") und2 = Undefined(name="y") assert und1 == und2 @@ -375,6 +375,7 @@ class TestUndefined: pytest.raises(UndefinedError, env.from_string("{{ missing }}").render) pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render) pytest.raises(UndefinedError, env.from_string("{{ missing|list }}").render) + pytest.raises(UndefinedError, env.from_string("{{ 'foo' in missing }}").render) assert env.from_string("{{ missing is not defined }}").render() == "True" pytest.raises( UndefinedError, env.from_string("{{ foo.missing }}").render, foo=42 diff --git a/tests/test_compile.py b/tests/test_compile.py new file mode 100644 index 0000000..42a773f --- /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 diff --git a/tests/test_regression.py b/tests/test_regression.py index 4491dab..7e23369 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -746,6 +746,13 @@ End""" tmpl = env.get_template("base") assert tmpl.render() == "42 y" + def test_nested_loop_scoping(self, env): + tmpl = env.from_string( + "{% set output %}{% for x in [1,2,3] %}hello{% endfor %}" + "{% endset %}{{ output }}" + ) + assert tmpl.render() == "hellohellohello" + @pytest.mark.parametrize("unicode_char", ["\N{FORM FEED}", "\x85"]) def test_unicode_whitespace(env, unicode_char): |