summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst7
-rw-r--r--CONTRIBUTING.rst10
-rw-r--r--README.rst2
-rw-r--r--src/jinja2/compiler.py7
-rw-r--r--src/jinja2/idtracking.py2
-rw-r--r--src/jinja2/runtime.py3
-rw-r--r--tests/test_api.py3
-rw-r--r--tests/test_compile.py28
-rw-r--r--tests/test_regression.py7
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.
diff --git a/README.rst b/README.rst
index f3a0d62..a197aea 100644
--- a/README.rst
+++ b/README.rst
@@ -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):