]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
Add ChainableUndefined allowing getattr & getitem (#997)
authorÉtienne Pelletier <EtiennePelletier@users.noreply.github.com>
Wed, 8 May 2019 14:47:33 +0000 (10:47 -0400)
committerDavid Lord <davidism@gmail.com>
Wed, 8 May 2019 14:47:33 +0000 (10:47 -0400)
* 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

.gitignore
CHANGES.rst
docs/api.rst
jinja2/__init__.py
jinja2/filters.py
jinja2/runtime.py
tests/test_api.py

index 28588b4203e87220f2e5d7b1fc2ce2dbf5d0ecd1..8402c1d3ef064b18f3a781b114b9395ae8585f2b 100644 (file)
@@ -10,6 +10,7 @@ dist/
 .tox/
 .cache/
 .idea/
+env/
 venv/
 venv-*/
 .coverage
index a41b887dd0ebe1a15ace0ecf1b5e93a17f921b30..0042a501040675e4e08f41aee76a1a68998685be 100644 (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
index ff0e3b21a14bd455f00cd17dbaccc5c169bc9a80..8672a42cc81e4210ed1eacbb4ab801565251a677 100644 (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()
index f20c573fd0a7b2952a2ae404324f5706744f5458..ae3587aff8cf625d5fe7a143e82c4cd8f743d191 100644 (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, \
index bf5173c1a32db9fe82802f4a884b75f73c12dff4..b55e1762ebe0241fef26135cab3acc6881419fae 100644 (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
index 5e313369edcb7e4fb83f7f8d6b08ef69baae24a2..b1972f8c7d4d5b0a04447ba70bc1251960d68292 100644 (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__
index 1354a7fb9c0b6dd48f57d07852c0263bdc4ca2bf..47dc4095d3c7ae0e9c5358bdd76938a91432968f 100644 (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 }}'