diff options
-rw-r--r-- | CHANGES.rst | 6 | ||||
-rw-r--r-- | jinja2/environment.py | 16 | ||||
-rw-r--r-- | jinja2/exceptions.py | 30 | ||||
-rw-r--r-- | jinja2/runtime.py | 43 | ||||
-rw-r--r-- | tests/test_api.py | 25 |
5 files changed, 95 insertions, 25 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 945e57f..d0f5d6d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -94,7 +94,11 @@ Unreleased that were previously overlooked. :issue:`733` - ``TemplateSyntaxError.source`` is not empty when raised from an included template. :issue:`457` - +- Passing an ``Undefined`` value to ``get_template`` (such as through + ``extends``, ``import``, or ``include``), raises an + ``UndefinedError`` consistently. ``select_template`` will show the + undefined message in the list of attempts rather than the empty + string. :issue:`1037` Version 2.10.3 -------------- diff --git a/jinja2/environment.py b/jinja2/environment.py index 2bfc018..8a9548a 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -25,7 +25,7 @@ from jinja2.nodes import EvalContext from jinja2.compiler import generate, CodeGenerator from jinja2.runtime import Undefined, new_context, Context from jinja2.exceptions import TemplateSyntaxError, TemplateNotFound, \ - TemplatesNotFound, TemplateRuntimeError + TemplatesNotFound, TemplateRuntimeError, UndefinedError from jinja2.utils import import_string, LRUCache, Markup, missing, \ concat, consume, internalcode, have_async_gen from jinja2._compat import imap, ifilter, string_types, iteritems, \ @@ -812,12 +812,20 @@ class Environment(object): before it fails. If it cannot find any of the templates, it will raise a :exc:`TemplatesNotFound` exception. - .. versionadded:: 2.3 + .. versionchanged:: 2.11 + If names is :class:`Undefined`, an :exc:`UndefinedError` is + raised instead. If no templates were found and names + contains :class:`Undefined`, the message is more helpful. .. versionchanged:: 2.4 If `names` contains a :class:`Template` object it is returned from the function unchanged. + + .. versionadded:: 2.3 """ + if isinstance(names, Undefined): + names._fail_with_undefined_error() + if not names: raise TemplatesNotFound(message=u'Tried to select from an empty list ' u'of templates.') @@ -829,7 +837,7 @@ class Environment(object): name = self.join_path(name, parent) try: return self._load_template(name, globals) - except TemplateNotFound: + except (TemplateNotFound, UndefinedError): pass raise TemplatesNotFound(names) @@ -842,7 +850,7 @@ class Environment(object): .. versionadded:: 2.3 """ - if isinstance(template_name_or_list, string_types): + if isinstance(template_name_or_list, (string_types, Undefined)): return self.get_template(template_name_or_list, parent, globals) elif isinstance(template_name_or_list, Template): return template_name_or_list diff --git a/jinja2/exceptions.py b/jinja2/exceptions.py index c9f08ac..36e4716 100644 --- a/jinja2/exceptions.py +++ b/jinja2/exceptions.py @@ -43,7 +43,12 @@ class TemplateError(Exception): @implements_to_string class TemplateNotFound(IOError, LookupError, TemplateError): - """Raised if a template does not exist.""" + """Raised if a template does not exist. + + .. versionchanged:: 2.11 + If the given name is :class:`Undefined` and no message was + provided, an :exc:`UndefinedError` is raised. + """ # looks weird, but removes the warning descriptor that just # bogusly warns us about message being deprecated @@ -51,8 +56,15 @@ class TemplateNotFound(IOError, LookupError, TemplateError): def __init__(self, name, message=None): IOError.__init__(self, name) + if message is None: + from jinja2.runtime import Undefined + + if isinstance(name, Undefined): + name._fail_with_undefined_error() + message = name + self.message = message self.name = name self.templates = [name] @@ -66,13 +78,27 @@ class TemplatesNotFound(TemplateNotFound): are selected. This is a subclass of :class:`TemplateNotFound` exception, so just catching the base exception will catch both. + .. versionchanged:: 2.11 + If a name in the list of names is :class:`Undefined`, a message + about it being undefined is shown rather than the empty string. + .. versionadded:: 2.2 """ def __init__(self, names=(), message=None): if message is None: + from jinja2.runtime import Undefined + + parts = [] + + for name in names: + if isinstance(name, Undefined): + parts.append(name._undefined_message) + else: + parts.append(name) + message = u'none of the templates given were found: ' + \ - u', '.join(imap(text_type, names)) + u', '.join(imap(text_type, parts)) TemplateNotFound.__init__(self, names and names[-1] or None, message) self.templates = list(names) diff --git a/jinja2/runtime.py b/jinja2/runtime.py index cb0bcf5..19ff838 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -668,27 +668,34 @@ class Undefined(object): self._undefined_name = name self._undefined_exception = exc + @property + def _undefined_message(self): + """Build a message about the undefined value based on how it was + accessed. + """ + if self._undefined_hint: + return self._undefined_hint + + if self._undefined_obj is missing: + return '%r is undefined' % self._undefined_name + + if not isinstance(self._undefined_name, string_types): + return '%s has no element %r' % ( + object_type_repr(self._undefined_obj), + self._undefined_name + ) + + return '%r has no attribute %r' % ( + object_type_repr(self._undefined_obj), + self._undefined_name + ) + @internalcode def _fail_with_undefined_error(self, *args, **kwargs): - """Regular callback function for undefined objects that raises an - `UndefinedError` on call. + """Raise an :exc:`UndefinedError` when operations are performed + on the undefined value. """ - if self._undefined_hint is None: - if self._undefined_obj is missing: - hint = '%r is undefined' % self._undefined_name - elif not isinstance(self._undefined_name, string_types): - hint = '%s has no element %r' % ( - object_type_repr(self._undefined_obj), - self._undefined_name - ) - else: - hint = '%r has no attribute %r' % ( - object_type_repr(self._undefined_obj), - self._undefined_name - ) - else: - hint = self._undefined_hint - raise self._undefined_exception(hint) + raise self._undefined_exception(self._undefined_message) @internalcode def __getattr__(self, name): diff --git a/tests/test_api.py b/tests/test_api.py index 2eb9ce7..6d31885 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -17,6 +17,7 @@ import pytest from jinja2 import Environment, Undefined, ChainableUndefined, \ DebugUndefined, StrictUndefined, UndefinedError, meta, \ is_undefined, Template, DictLoader, make_logging_undefined +from jinja2 import TemplatesNotFound from jinja2.compiler import CodeGenerator from jinja2.runtime import Context from jinja2.utils import Cycler @@ -125,6 +126,30 @@ class TestExtendedAPI(object): assert env.get_or_select_template([t]) is t assert env.get_or_select_template(t) is t + def test_get_template_undefined(self, env): + """Passing Undefined to get/select_template raises an + UndefinedError or shows the undefined message in the list. + """ + env.loader=DictLoader({}) + t = Undefined(name="no_name_1") + + with pytest.raises(UndefinedError): + env.get_template(t) + + with pytest.raises(UndefinedError): + env.get_or_select_template(t) + + with pytest.raises(UndefinedError): + env.select_template(t) + + with pytest.raises(TemplatesNotFound) as exc_info: + env.select_template([t, "no_name_2"]) + + exc_message = str(exc_info.value) + assert "'no_name_1' is undefined" in exc_message + assert "no_name_2" in exc_message + + def test_autoescape_autoselect(self, env): def select_autoescape(name): if name is None or '.' not in name: |