:issue:`1921`
- Make compiling deterministic for tuple unpacking in a ``{% set ... %}``
call. :issue:`2021`
+- Fix dunder protocol (`copy`/`pickle`/etc) interaction with ``Undefined``
+ objects. :issue:`2025`
Version 3.1.4
@internalcode
def __getattr__(self, name: str) -> t.Any:
- if name[:2] == "__":
+ # Raise AttributeError on requests for names that appear to be unimplemented
+ # dunder methods to keep Python's internal protocol probing behaviors working
+ # properly in cases where another exception type could cause unexpected or
+ # difficult-to-diagnose failures.
+ if name[:2] == "__" and name[-2:] == "__":
raise AttributeError(name)
return self._fail_with_undefined_error()
def __html__(self) -> str:
return str(self)
- def __getattr__(self, _: str) -> "ChainableUndefined":
+ def __getattr__(self, name: str) -> "ChainableUndefined":
+ # Raise AttributeError on requests for names that appear to be unimplemented
+ # dunder methods to avoid confusing Python with truthy non-method objects that
+ # do not implement the protocol being probed for. e.g., copy.copy(Undefined())
+ # fails spectacularly if getattr(Undefined(), '__setstate__') returns an
+ # Undefined object instead of raising AttributeError to signal that it does not
+ # support that style of object initialization.
+ if name[:2] == "__" and name[-2:] == "__":
+ raise AttributeError(name)
+
return self
- __getitem__ = __getattr__ # type: ignore
+ def __getitem__(self, _name: str) -> "ChainableUndefined": # type: ignore[override]
+ return self
class DebugUndefined(Undefined):
__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
-# unneeded and contain wrong data for subclasses.
-del (
- Undefined.__slots__,
- ChainableUndefined.__slots__,
- DebugUndefined.__slots__,
- StrictUndefined.__slots__,
-)
assert und1 == und2
assert und1 != 42
assert hash(und1) == hash(und2) == hash(Undefined())
- with pytest.raises(AttributeError):
- getattr(Undefined, "__slots__") # noqa: B009
def test_chainable_undefined(self):
env = Environment(undefined=ChainableUndefined)
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)
- with pytest.raises(AttributeError):
- getattr(ChainableUndefined, "__slots__") # noqa: B009
# The following tests ensure subclass functionality works as expected
assert env.from_string('{{ missing.bar["baz"] }}').render() == ""
str(DebugUndefined(hint=undefined_hint))
== f"{{{{ undefined value printed: {undefined_hint} }}}}"
)
- with pytest.raises(AttributeError):
- getattr(DebugUndefined, "__slots__") # noqa: B009
def test_strict_undefined(self):
env = Environment(undefined=StrictUndefined)
env.from_string('{{ missing|default("default", true) }}').render()
== "default"
)
- with pytest.raises(AttributeError):
- getattr(StrictUndefined, "__slots__") # noqa: B009
assert env.from_string('{{ "foo" if false }}').render() == ""
def test_indexing_gives_undefined(self):
+import copy
import itertools
+import pickle
+import pytest
+
+from jinja2 import ChainableUndefined
+from jinja2 import DebugUndefined
+from jinja2 import StrictUndefined
from jinja2 import Template
+from jinja2 import TemplateRuntimeError
+from jinja2 import Undefined
from jinja2.runtime import LoopContext
TEST_IDX_TEMPLATE_STR_1 = (
out = t.render(calc=Calc())
# Would be "1" if context argument was passed.
assert out == "0"
+
+
+_undefined_types = (Undefined, ChainableUndefined, DebugUndefined, StrictUndefined)
+
+
+@pytest.mark.parametrize("undefined_type", _undefined_types)
+def test_undefined_copy(undefined_type):
+ undef = undefined_type("a hint", ["foo"], "a name", TemplateRuntimeError)
+ copied = copy.copy(undef)
+
+ assert copied is not undef
+ assert copied._undefined_hint is undef._undefined_hint
+ assert copied._undefined_obj is undef._undefined_obj
+ assert copied._undefined_name is undef._undefined_name
+ assert copied._undefined_exception is undef._undefined_exception
+
+
+@pytest.mark.parametrize("undefined_type", _undefined_types)
+def test_undefined_deepcopy(undefined_type):
+ undef = undefined_type("a hint", ["foo"], "a name", TemplateRuntimeError)
+ copied = copy.deepcopy(undef)
+
+ assert copied._undefined_hint is undef._undefined_hint
+ assert copied._undefined_obj is not undef._undefined_obj
+ assert copied._undefined_obj == undef._undefined_obj
+ assert copied._undefined_name is undef._undefined_name
+ assert copied._undefined_exception is undef._undefined_exception
+
+
+@pytest.mark.parametrize("undefined_type", _undefined_types)
+def test_undefined_pickle(undefined_type):
+ undef = undefined_type("a hint", ["foo"], "a name", TemplateRuntimeError)
+ copied = pickle.loads(pickle.dumps(undef))
+
+ assert copied._undefined_hint is not undef._undefined_hint
+ assert copied._undefined_hint == undef._undefined_hint
+ assert copied._undefined_obj is not undef._undefined_obj
+ assert copied._undefined_obj == undef._undefined_obj
+ assert copied._undefined_name is not undef._undefined_name
+ assert copied._undefined_name == undef._undefined_name
+ assert copied._undefined_exception is undef._undefined_exception