summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÉtienne Pelletier <EtiennePelletier@users.noreply.github.com>2019-05-08 10:47:33 -0400
committerDavid Lord <davidism@gmail.com>2019-05-08 10:47:33 -0400
commit19133d40593ced72eb28e230588abcc70d8b9f82 (patch)
treea92f5b265e9508c4ab6f5a197b3a391e7e35b281
parent9766c179fad831aa6aa2039882fadc7aff6bba2d (diff)
downloadjinja2-19133d40593ced72eb28e230588abcc70d8b9f82.tar.gz
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
-rw-r--r--.gitignore1
-rw-r--r--CHANGES.rst5
-rw-r--r--docs/api.rst4
-rw-r--r--jinja2/__init__.py4
-rw-r--r--jinja2/filters.py6
-rw-r--r--jinja2/runtime.py33
-rw-r--r--tests/test_api.py25
7 files changed, 69 insertions, 9 deletions
diff --git a/.gitignore b/.gitignore
index 28588b4..8402c1d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ dist/
.tox/
.cache/
.idea/
+env/
venv/
venv-*/
.coverage
diff --git a/CHANGES.rst b/CHANGES.rst
index a41b887..0042a50 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -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
diff --git a/docs/api.rst b/docs/api.rst
index ff0e3b2..8672a42 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -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()
diff --git a/jinja2/__init__.py b/jinja2/__init__.py
index f20c573..ae3587a 100644
--- a/jinja2/__init__.py
+++ b/jinja2/__init__.py
@@ -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, \
diff --git a/jinja2/filters.py b/jinja2/filters.py
index bf5173c..b55e176 100644
--- a/jinja2/filters.py
+++ b/jinja2/filters.py
@@ -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
diff --git a/jinja2/runtime.py b/jinja2/runtime.py
index 5e31336..b1972f8 100644
--- a/jinja2/runtime.py
+++ b/jinja2/runtime.py
@@ -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__
diff --git a/tests/test_api.py b/tests/test_api.py
index 1354a7f..47dc409 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -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 }}'