preserve __slots__ on Undefined classes
This commit is contained in:
parent
39d9ffff1f
commit
d4fb0e8c40
@ -22,6 +22,8 @@ Unreleased
|
||||
: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
|
||||
|
||||
@ -860,7 +860,11 @@ class Undefined:
|
||||
|
||||
@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()
|
||||
@ -984,10 +988,20 @@ class ChainableUndefined(Undefined):
|
||||
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):
|
||||
@ -1046,13 +1060,3 @@ class StrictUndefined(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__,
|
||||
)
|
||||
|
||||
@ -323,8 +323,6 @@ class TestUndefined:
|
||||
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)
|
||||
@ -335,8 +333,6 @@ 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)
|
||||
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() == ""
|
||||
@ -368,8 +364,6 @@ class TestUndefined:
|
||||
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)
|
||||
@ -386,8 +380,6 @@ class TestUndefined:
|
||||
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):
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
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 = (
|
||||
@ -73,3 +82,44 @@ def test_mock_not_pass_arg_marker():
|
||||
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user