diff options
author | Étienne Pelletier <EtiennePelletier@users.noreply.github.com> | 2019-05-08 10:47:33 -0400 |
---|---|---|
committer | David Lord <davidism@gmail.com> | 2019-05-08 10:47:33 -0400 |
commit | 19133d40593ced72eb28e230588abcc70d8b9f82 (patch) | |
tree | a92f5b265e9508c4ab6f5a197b3a391e7e35b281 | |
parent | 9766c179fad831aa6aa2039882fadc7aff6bba2d (diff) | |
download | jinja2-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-- | .gitignore | 1 | ||||
-rw-r--r-- | CHANGES.rst | 5 | ||||
-rw-r--r-- | docs/api.rst | 4 | ||||
-rw-r--r-- | jinja2/__init__.py | 4 | ||||
-rw-r--r-- | jinja2/filters.py | 6 | ||||
-rw-r--r-- | jinja2/runtime.py | 33 | ||||
-rw-r--r-- | tests/test_api.py | 25 |
7 files changed, 69 insertions, 9 deletions
@@ -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 }}' |