Repository: pallets/jinja (python)

Issue:
containment should only fail for `StrictUndefined`, not regular `Undefined`
### Summary
Hello, I'm looking into a change of behaviour between jinja2-2.11.x (and previous... this section of our template has been around for a long time) and jinja2-3.0.x.  I have a feeling it was a bug in 2.11 that got fixed but since I don't see the behaviour change spelled out in the changelog I decided to report it in case it was an unintended side effect of a different bugfix.

With jinja2-2.11.x, certain `{% if x in dictionary[key] %}` constructs will evaluate False if key is not present in the dictionary.  In jinja2-3.0.x, the construct will raise an UndefinedException instead. 

### Reproducer

Here's a small template that triggers the behaviour:
``` jinja
{# Called with d = {'test': {}} #}
Start
{% for entry, value in d.items() %}
{%     if 'n' in value.a %}
    Condition triggered
{%     endif %}
{% endfor %}
End
```
And here's my test script with the template embedded in a python string:
``` python
#!/usr/bin/python3    
    
import jinja2    
data = """    
{# Called with d = {'test': {}} #}
Start
{% for entry, value in d.items() %}
{%     if 'n' in value.a %} 
Condition triggered
{%     endif %}
{% endfor %}
End
"""    
t = jinja2.Template(data)    
print(t.render(d={'test': {}}))
```

#### Behaviour in 2.11
```
    

Start



End
```

#### Behaviour in 3.0

```
Traceback (most recent call last):
  File "/srv/ansible/jinja2-3.x-bug/test2.py", line 15, in <module>
    print(t.render(d={'test': {}}))
  File "/home/badger/.local/lib/python3.9/site-packages/jinja2/environment.py", line 1304, in render
    self.environment.handle_exception()
  File "/home/badger/.local/lib/python3.9/site-packages/jinja2/environment.py", line 925, in handle_exception
    raise rewrite_traceback_stack(source=source)
  File "<template>", line 5, in top-level template code
jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'a'
```
### Additional information

I have done a little bit of experimentation and the change is caused by https://github.com/pallets/jinja/pull/1204/ (If I remove that change from a jinja2-3.0.1 install, it reverts to the 2.11.x behaviour).  However, since my template does not traceback with a TypeError in 2.11.x (which is the bug being fixed by that PR: https://github.com/pallets/jinja/issues/1198  ), I'm unsure if the new behaviour is desired or not.

Personally, I feel the new behaviour is more intuitive and want to point it out to people as a reason they should normalize their data before passing it off to be rendered but I just wanted to make sure it was an intended change before I open my big mouth ;-)


Environment:

- Python version: Tested in both 3.9 and 3.8
- Jinja version: 2.11.2 versus 3.0.1



Pull request:
Only StrictUndefined.__contains__ raises error
- fixes #1448 

<!--
Ensure each step in CONTRIBUTING.rst is complete by adding an "x" to each box below.

If only docs were changed, these aren't relevant and can be removed.
-->

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [ ] Add or update relevant docs, in the docs folder and in code.
	- There was no docs added when this was set, so there shouldn't be doc to remove or change.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [ ] Add `.. versionchanged::` entries in any relevant code docs.
    - No docstring changed
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 116461af..074dd1f8 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -3,12 +3,12 @@
 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/src/jinja2/runtime.py b/src/jinja2/runtime.py
index 87bb1327..2346cf2b 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


Test patch:
diff --git a/tests/test_api.py b/tests/test_api.py
index 774bb3c9..4db3b4a9 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