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:
parent
9766c179fa
commit
19133d4059
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@ dist/
|
||||
.tox/
|
||||
.cache/
|
||||
.idea/
|
||||
env/
|
||||
venv/
|
||||
venv-*/
|
||||
.coverage
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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, \
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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__
|
||||
|
||||
@ -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 }}'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user