Add ChainableUndefined allowing getattr & getitem (#997)

* Add ChainableUndefined allowing getattr & getitem

Allows using default values with chains of items or attributes that may
contain undefined values without raising a jinja2.exceptions.UndefinedError.

>>> import jinja2
>>> env = jinja2.Environment(undefined=jinja2.ChainableUndefined)
>>> env.from_string("{{ foo.bar['baz'] | default('val') }}").render()
'val'

* Remove class decorator from ChainableUndefined
This commit is contained in:
Étienne Pelletier 2019-05-08 10:47:33 -04:00 committed by David Lord
parent 9766c179fa
commit 19133d4059
7 changed files with 69 additions and 9 deletions

1
.gitignore vendored
View File

@ -10,6 +10,7 @@ dist/
.tox/
.cache/
.idea/
env/
venv/
venv-*/
.coverage

View File

@ -13,9 +13,12 @@ unreleased
:class:`~environment.Environment` enables it, in order to avoid a
slow initial import. (`#765`_)
- Python 2.6 and 3.3 are not supported anymore.
- The `map` filter in async mode now automatically awaits
- The ``map`` filter in async mode now automatically awaits
- Added a new ``ChainableUndefined`` class to support getitem
and getattr on an undefined object. (`#977`_)
.. _#765: https://github.com/pallets/jinja/issues/765
.. _#977: https://github.com/pallets/jinja/issues/977
Version 2.10.1

View File

@ -322,7 +322,7 @@ unable to look up a name or access an attribute one of those objects is
created and returned. Some operations on undefined values are then allowed,
others fail.
The closest to regular Python behavior is the `StrictUndefined` which
The closest to regular Python behavior is the :class:`StrictUndefined` which
disallows all operations beside testing if it's an undefined object.
.. autoclass:: jinja2.Undefined()
@ -353,6 +353,8 @@ disallows all operations beside testing if it's an undefined object.
:attr:`_undefined_exception` with an error message generated
from the undefined hints stored on the undefined object.
.. autoclass:: jinja2.ChainableUndefined()
.. autoclass:: jinja2.DebugUndefined()
.. autoclass:: jinja2.StrictUndefined()

View File

@ -42,8 +42,8 @@ from jinja2.bccache import BytecodeCache, FileSystemBytecodeCache, \
MemcachedBytecodeCache
# undefined types
from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined, \
make_logging_undefined
from jinja2.runtime import Undefined, ChainableUndefined, DebugUndefined, \
StrictUndefined, make_logging_undefined
# exceptions
from jinja2.exceptions import TemplateError, UndefinedError, \

View File

@ -368,6 +368,12 @@ def do_default(value, default_value=u'', boolean=False):
.. sourcecode:: jinja
{{ ''|default('the string was empty', true) }}
.. versionchanged:: 2.11
It's now possible to configure the :class:`~jinja2.Environment` with
:class:`~jinja2.ChainableUndefined` to make the `default` filter work
on nested elements and attributes that may contain undefined values
in the chain without getting an :exc:`~jinja2.UndefinedError`.
"""
if isinstance(value, Undefined) or (boolean and not value):
return default_value

View File

@ -586,7 +586,7 @@ class Macro(object):
@implements_to_string
class Undefined(object):
"""The default undefined type. This undefined type can be printed and
iterated over, but every other access will raise an :exc:`jinja2.exceptions.UndefinedError`:
iterated over, but every other access will raise an :exc:`UndefinedError`:
>>> foo = Undefined(name='foo')
>>> str(foo)
@ -610,7 +610,7 @@ class Undefined(object):
@internalcode
def _fail_with_undefined_error(self, *args, **kwargs):
"""Regular callback function for undefined objects that raises an
`jinja2.exceptions.UndefinedError` on call.
`UndefinedError` on call.
"""
if self._undefined_hint is None:
if self._undefined_obj is missing:
@ -750,6 +750,32 @@ def make_logging_undefined(logger=None, base=None):
return LoggingUndefined
# No @implements_to_string decorator here because __str__
# is not overwritten from Undefined in this class.
# This would cause a recursion error in Python 2.
class ChainableUndefined(Undefined):
"""An undefined that is chainable, where both
__getattr__ and __getitem__ return itself rather than
raising an :exc:`UndefinedError`:
>>> foo = ChainableUndefined(name='foo')
>>> str(foo.bar['baz'])
''
>>> foo.bar['baz'] + 42
Traceback (most recent call last):
...
jinja2.exceptions.UndefinedError: 'foo' is undefined
.. versionadded:: 2.11
"""
__slots__ = ()
def __getattr__(self, _):
return self
__getitem__ = __getattr__
@implements_to_string
class DebugUndefined(Undefined):
"""An undefined that returns the debug info when printed.
@ -805,4 +831,5 @@ class StrictUndefined(Undefined):
# remove remaining slots attributes, after the metaclass did the magic they
# are unneeded and irritating as they contain wrong data for the subclasses.
del Undefined.__slots__, DebugUndefined.__slots__, StrictUndefined.__slots__
del Undefined.__slots__, ChainableUndefined.__slots__, \
DebugUndefined.__slots__, StrictUndefined.__slots__

View File

@ -13,8 +13,8 @@ import tempfile
import shutil
import pytest
from jinja2 import Environment, Undefined, DebugUndefined, \
StrictUndefined, UndefinedError, meta, \
from jinja2 import Environment, Undefined, ChainableUndefined, \
DebugUndefined, StrictUndefined, UndefinedError, meta, \
is_undefined, Template, DictLoader, make_logging_undefined
from jinja2.compiler import CodeGenerator
from jinja2.runtime import Context
@ -258,6 +258,27 @@ class TestUndefined(object):
pytest.raises(UndefinedError,
env.from_string('{{ missing - 1}}').render)
def test_chainable_undefined(self):
env = Environment(undefined=ChainableUndefined)
# The following tests are copied from test_default_undefined
assert env.from_string('{{ missing }}').render() == u''
assert env.from_string('{{ missing|list }}').render() == '[]'
assert env.from_string('{{ missing is not defined }}').render() \
== 'True'
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)
# The following tests ensure subclass functionality works as expected
assert env.from_string('{{ missing.bar["baz"] }}').render() == u''
assert env.from_string('{{ foo.bar["baz"]._undefined_name }}').render() \
== u'foo'
assert env.from_string('{{ foo.bar["baz"]._undefined_name }}').render(
foo=42) == u'bar'
assert env.from_string('{{ foo.bar["baz"]._undefined_name }}').render(
foo={'bar': 42}) == u'baz'
def test_debug_undefined(self):
env = Environment(undefined=DebugUndefined)
assert env.from_string('{{ missing }}').render() == '{{ missing }}'