mirror of
https://gitee.com/openharmony/third_party_jinja2
synced 2025-02-09 01:07:52 +00:00
Merge pull request #1383 from pallets/test-decorators
add 'is filter' and 'is test' tests
This commit is contained in:
commit
beabf304b0
@ -41,6 +41,11 @@ Unreleased
|
||||
already loaded. :issue:`295`
|
||||
- Do not raise an error for undefined filters in unexecuted
|
||||
if-statements and conditional expressions. :issue:`842`
|
||||
- Add ``is filter`` and ``is test`` tests to test if a name is a
|
||||
registered filter or test. This allows checking if a filter is
|
||||
available in a template before using it. Test functions can be
|
||||
decorated with ``@environmentfunction``, ``@evalcontextfunction``,
|
||||
or ``@contextfunction``. :issue:`842`, :pr:`1248`
|
||||
|
||||
|
||||
Version 2.11.3
|
||||
|
171
docs/api.rst
171
docs/api.rst
@ -666,56 +666,119 @@ Exceptions
|
||||
Custom Filters
|
||||
--------------
|
||||
|
||||
Custom filters are just regular Python functions that take the left side of
|
||||
the filter as first argument and the arguments passed to the filter as
|
||||
extra arguments or keyword arguments.
|
||||
Filters are Python functions that take the value to the left of the
|
||||
filter as the first argument and produce a new value. Arguments passed
|
||||
to the filter are passed after the value.
|
||||
|
||||
For example in the filter ``{{ 42|myfilter(23) }}`` the function would be
|
||||
called with ``myfilter(42, 23)``. Here for example a simple filter that can
|
||||
be applied to datetime objects to format them::
|
||||
For example, the filter ``{{ 42|myfilter(23) }}`` is called behind the
|
||||
scenes as ``myfilter(42, 23)``.
|
||||
|
||||
def datetimeformat(value, format='%H:%M / %d-%m-%Y'):
|
||||
Jinja comes with some :ref:`built-in filters <builtin-filters>`. To use
|
||||
a custom filter, write a function that takes at least a ``value``
|
||||
argument, then register it in :attr:`Environment.filters`.
|
||||
|
||||
Here's a filter that formats datetime objects:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def datetime_format(value, format="%H:%M %d-%m-%y"):
|
||||
return value.strftime(format)
|
||||
|
||||
You can register it on the template environment by updating the
|
||||
:attr:`~Environment.filters` dict on the environment::
|
||||
environment.filters["datetime_format"] = datetime_format
|
||||
|
||||
environment.filters['datetimeformat'] = datetimeformat
|
||||
|
||||
Inside the template it can then be used as follows:
|
||||
Now it can be used in templates:
|
||||
|
||||
.. sourcecode:: jinja
|
||||
|
||||
written on: {{ article.pub_date|datetimeformat }}
|
||||
publication date: {{ article.pub_date|datetimeformat('%d-%m-%Y') }}
|
||||
{{ article.pub_date|datetimeformat }}
|
||||
{{ article.pub_date|datetimeformat("%B %Y") }}
|
||||
|
||||
Filters can also be passed the current template context or environment. This
|
||||
is useful if a filter wants to return an undefined value or check the current
|
||||
:attr:`~Environment.autoescape` setting. For this purpose three decorators
|
||||
exist: :func:`environmentfilter`, :func:`contextfilter` and
|
||||
:func:`evalcontextfilter`.
|
||||
Some decorators are available to tell Jinja to pass extra information to
|
||||
the filter. The object is passed as the first argument, making the value
|
||||
being filtered the second argument.
|
||||
|
||||
Here a small example filter that breaks a text into HTML line breaks and
|
||||
paragraphs and marks the return value as safe HTML string if autoescaping is
|
||||
enabled::
|
||||
- :func:`environmentfilter` passes the :class:`Environment`.
|
||||
- :func:`evalcontextfilter` passes the :ref:`eval-context`.
|
||||
- :func:`contextfilter` passes the current
|
||||
:class:`~jinja2.runtime.Context`.
|
||||
|
||||
Here's a filter that converts line breaks into HTML ``<br>`` and ``<p>``
|
||||
tags. It uses the eval context to check if autoescape is currently
|
||||
enabled before escaping the input and marking the output safe.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import re
|
||||
from jinja2 import evalcontextfilter, Markup, escape
|
||||
|
||||
_paragraph_re = re.compile(r"(?:\r\n|\r(?!\n)|\n){2,}")
|
||||
from jinja2 import evalcontextfilter
|
||||
from markupsafe import Markup, escape
|
||||
|
||||
@evalcontextfilter
|
||||
def nl2br(eval_ctx, value):
|
||||
result = "\n\n".join(
|
||||
f"<p>{p.replace('\n', Markup('<br>\n'))}</p>"
|
||||
for p in _paragraph_re.split(escape(value))
|
||||
)
|
||||
if eval_ctx.autoescape:
|
||||
result = Markup(result)
|
||||
return result
|
||||
br = "<br>\n"
|
||||
|
||||
Context filters work the same just that the first argument is the current
|
||||
active :class:`Context` rather than the environment.
|
||||
if eval_ctx.autoescape:
|
||||
value = escape(value)
|
||||
br = Markup(br)
|
||||
|
||||
result = "\n\n".join(
|
||||
f"<p>{br.join(p.splitlines())}<\p>"
|
||||
for p in re.split(r"(?:\r\n|\r(?!\n)|\n){2,}", value)
|
||||
)
|
||||
return Markup(result) if autoescape else result
|
||||
|
||||
|
||||
.. _writing-tests:
|
||||
|
||||
Custom Tests
|
||||
------------
|
||||
|
||||
Test are Python functions that take the value to the left of the test as
|
||||
the first argument, and return ``True`` or ``False``. Arguments passed
|
||||
to the test are passed after the value.
|
||||
|
||||
For example, the test ``{{ 42 is even }}`` is called behind the scenes
|
||||
as ``is_even(42)``.
|
||||
|
||||
Jinja comes with some :ref:`built-in tests <builtin-tests>`. To use a
|
||||
custom tests, write a function that takes at least a ``value`` argument,
|
||||
then register it in :attr:`Environment.tests`.
|
||||
|
||||
Here's a test that checks if a value is a prime number:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import math
|
||||
|
||||
def is_prime(n):
|
||||
if n == 2:
|
||||
return True
|
||||
|
||||
for i in range(2, int(math.ceil(math.sqrt(n))) + 1):
|
||||
if n % i == 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
environment.tests["prime"] = is_prime
|
||||
|
||||
Now it can be used in templates:
|
||||
|
||||
.. sourcecode:: jinja
|
||||
|
||||
{% if value is prime %}
|
||||
{{ value }} is a prime number
|
||||
{% else %}
|
||||
{{ value }} is not a prime number
|
||||
{% endif %}
|
||||
|
||||
Some decorators are available to tell Jinja to pass extra information to
|
||||
the filter. The object is passed as the first argument, making the value
|
||||
being filtered the second argument.
|
||||
|
||||
- :func:`environmentfunction` passes the :class:`Environment`.
|
||||
- :func:`evalcontextfunction` passes the :ref:`eval-context`.
|
||||
- :func:`contextfunction` passes the current
|
||||
:class:`~jinja2.runtime.Context`.
|
||||
|
||||
|
||||
.. _eval-context:
|
||||
@ -780,46 +843,6 @@ eval context object itself.
|
||||
time. At runtime this should always be `False`.
|
||||
|
||||
|
||||
.. _writing-tests:
|
||||
|
||||
Custom Tests
|
||||
------------
|
||||
|
||||
Tests work like filters just that there is no way for a test to get access
|
||||
to the environment or context and that they can't be chained. The return
|
||||
value of a test should be `True` or `False`. The purpose of a test is to
|
||||
give the template designers the possibility to perform type and conformability
|
||||
checks.
|
||||
|
||||
Here a simple test that checks if a variable is a prime number::
|
||||
|
||||
import math
|
||||
|
||||
def is_prime(n):
|
||||
if n == 2:
|
||||
return True
|
||||
for i in range(2, int(math.ceil(math.sqrt(n))) + 1):
|
||||
if n % i == 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
You can register it on the template environment by updating the
|
||||
:attr:`~Environment.tests` dict on the environment::
|
||||
|
||||
environment.tests['prime'] = is_prime
|
||||
|
||||
A template designer can then use the test like this:
|
||||
|
||||
.. sourcecode:: jinja
|
||||
|
||||
{% if 42 is prime %}
|
||||
42 is a prime number
|
||||
{% else %}
|
||||
42 is not a prime number
|
||||
{% endif %}
|
||||
|
||||
|
||||
.. _global-namespace:
|
||||
|
||||
The Global Namespace
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Compiles nodes from the parser into Python code."""
|
||||
import typing as t
|
||||
from collections import namedtuple
|
||||
from contextlib import contextmanager
|
||||
from functools import update_wrapper
|
||||
from io import StringIO
|
||||
from itertools import chain
|
||||
@ -440,15 +441,28 @@ class CodeGenerator(NodeVisitor):
|
||||
self.visit(node.dyn_kwargs, frame)
|
||||
|
||||
def pull_dependencies(self, nodes):
|
||||
"""Pull all the dependencies."""
|
||||
"""Find all filter and test names used in the template and
|
||||
assign them to variables in the compiled namespace. Checking
|
||||
that the names are registered with the environment is done when
|
||||
compiling the Filter and Test nodes. If the node is in an If or
|
||||
CondExpr node, the check is done at runtime instead.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
Filters and tests in If and CondExpr nodes are checked at
|
||||
runtime instead of compile time.
|
||||
"""
|
||||
visitor = DependencyFinderVisitor()
|
||||
|
||||
for node in nodes:
|
||||
visitor.visit(node)
|
||||
|
||||
for dependency in "filters", "tests":
|
||||
mapping = getattr(self, dependency)
|
||||
|
||||
for name in getattr(visitor, dependency):
|
||||
if name not in mapping:
|
||||
mapping[name] = self.temporary_identifier()
|
||||
|
||||
# add check during runtime that dependencies used inside of executed
|
||||
# blocks are defined, as this step may be skipped during compile time
|
||||
self.writeline("try:")
|
||||
@ -461,7 +475,8 @@ class CodeGenerator(NodeVisitor):
|
||||
self.writeline(f"def {mapping[name]}(*unused):")
|
||||
self.indent()
|
||||
self.writeline(
|
||||
f'raise TemplateRuntimeError("no filter named {name!r} found")'
|
||||
f'raise TemplateRuntimeError("No {dependency[:-1]}'
|
||||
f' named {name!r} found.")'
|
||||
)
|
||||
self.outdent()
|
||||
self.outdent()
|
||||
@ -1657,47 +1672,69 @@ class CodeGenerator(NodeVisitor):
|
||||
self.write(":")
|
||||
self.visit(node.step, frame)
|
||||
|
||||
@optimizeconst
|
||||
def visit_Filter(self, node, frame):
|
||||
@contextmanager
|
||||
def _filter_test_common(self, node, frame, is_filter):
|
||||
if is_filter:
|
||||
compiler_map = self.filters
|
||||
env_map = self.environment.filters
|
||||
type_name = mark_name = "filter"
|
||||
else:
|
||||
compiler_map = self.tests
|
||||
env_map = self.environment.tests
|
||||
type_name = "test"
|
||||
# Filters use "contextfilter", tests and calls use "contextfunction".
|
||||
mark_name = "function"
|
||||
|
||||
if self.environment.is_async:
|
||||
self.write("await auto_await(")
|
||||
self.write(self.filters[node.name] + "(")
|
||||
func = self.environment.filters.get(node.name)
|
||||
|
||||
self.write(compiler_map[node.name] + "(")
|
||||
func = env_map.get(node.name)
|
||||
|
||||
# When inside an If or CondExpr frame, allow the filter to be
|
||||
# undefined at compile time and only raise an error if it's
|
||||
# actually called at runtime. See pull_dependencies.
|
||||
if func is None and not frame.soft_frame:
|
||||
self.fail(f"no filter named {node.name!r}", node.lineno)
|
||||
if getattr(func, "contextfilter", False) is True:
|
||||
self.fail(f"No {type_name} named {node.name!r}.", node.lineno)
|
||||
|
||||
if getattr(func, f"context{mark_name}", False) is True:
|
||||
self.write("context, ")
|
||||
elif getattr(func, "evalcontextfilter", False) is True:
|
||||
elif getattr(func, f"evalcontext{mark_name}", False) is True:
|
||||
self.write("context.eval_ctx, ")
|
||||
elif getattr(func, "environmentfilter", False) is True:
|
||||
elif getattr(func, f"environment{mark_name}", False) is True:
|
||||
self.write("environment, ")
|
||||
|
||||
# if the filter node is None we are inside a filter block
|
||||
# and want to write to the current buffer
|
||||
if node.node is not None:
|
||||
self.visit(node.node, frame)
|
||||
elif frame.eval_ctx.volatile:
|
||||
self.write(
|
||||
f"(Markup(concat({frame.buffer}))"
|
||||
f" if context.eval_ctx.autoescape else concat({frame.buffer}))"
|
||||
)
|
||||
elif frame.eval_ctx.autoescape:
|
||||
self.write(f"Markup(concat({frame.buffer}))")
|
||||
else:
|
||||
self.write(f"concat({frame.buffer})")
|
||||
# Back to the visitor function to handle visiting the target of
|
||||
# the filter or test.
|
||||
yield
|
||||
|
||||
self.signature(node, frame)
|
||||
self.write(")")
|
||||
|
||||
if self.environment.is_async:
|
||||
self.write(")")
|
||||
|
||||
@optimizeconst
|
||||
def visit_Filter(self, node, frame):
|
||||
with self._filter_test_common(node, frame, True):
|
||||
# if the filter node is None we are inside a filter block
|
||||
# and want to write to the current buffer
|
||||
if node.node is not None:
|
||||
self.visit(node.node, frame)
|
||||
elif frame.eval_ctx.volatile:
|
||||
self.write(
|
||||
f"(Markup(concat({frame.buffer}))"
|
||||
f" if context.eval_ctx.autoescape else concat({frame.buffer}))"
|
||||
)
|
||||
elif frame.eval_ctx.autoescape:
|
||||
self.write(f"Markup(concat({frame.buffer}))")
|
||||
else:
|
||||
self.write(f"concat({frame.buffer})")
|
||||
|
||||
@optimizeconst
|
||||
def visit_Test(self, node, frame):
|
||||
self.write(self.tests[node.name] + "(")
|
||||
if node.name not in self.environment.tests:
|
||||
self.fail(f"no test named {node.name!r}", node.lineno)
|
||||
self.visit(node.node, frame)
|
||||
self.signature(node, frame)
|
||||
self.write(")")
|
||||
with self._filter_test_common(node, frame, False):
|
||||
self.visit(node.node, frame)
|
||||
|
||||
@optimizeconst
|
||||
def visit_CondExpr(self, node, frame):
|
||||
|
@ -102,17 +102,6 @@ def load_extensions(environment, extensions):
|
||||
return result
|
||||
|
||||
|
||||
def fail_for_missing_callable(thing, name):
|
||||
msg = f"no {thing} named {name!r}"
|
||||
|
||||
if isinstance(name, Undefined):
|
||||
try:
|
||||
name._fail_with_undefined_error()
|
||||
except Exception as e:
|
||||
msg = f"{msg} ({e}; did you forget to quote the callable name?)"
|
||||
raise TemplateRuntimeError(msg)
|
||||
|
||||
|
||||
def _environment_sanity_check(environment):
|
||||
"""Perform a sanity check on the environment."""
|
||||
assert issubclass(
|
||||
@ -470,10 +459,58 @@ class Environment:
|
||||
except (TypeError, LookupError, AttributeError):
|
||||
return self.undefined(obj=obj, name=attribute)
|
||||
|
||||
def _filter_test_common(
|
||||
self, name, value, args, kwargs, context, eval_ctx, is_filter
|
||||
):
|
||||
if is_filter:
|
||||
env_map = self.filters
|
||||
type_name = mark_name = "filter"
|
||||
else:
|
||||
env_map = self.tests
|
||||
type_name = "test"
|
||||
# Filters use "contextfilter", tests and calls use "contextfunction".
|
||||
mark_name = "function"
|
||||
|
||||
func = env_map.get(name)
|
||||
|
||||
if func is None:
|
||||
msg = f"No {type_name} named {name!r}."
|
||||
|
||||
if isinstance(name, Undefined):
|
||||
try:
|
||||
name._fail_with_undefined_error()
|
||||
except Exception as e:
|
||||
msg = f"{msg} ({e}; did you forget to quote the callable name?)"
|
||||
|
||||
raise TemplateRuntimeError(msg)
|
||||
|
||||
args = [value, *(args if args is not None else ())]
|
||||
kwargs = kwargs if kwargs is not None else {}
|
||||
|
||||
if getattr(func, f"context{mark_name}", False) is True:
|
||||
if context is None:
|
||||
raise TemplateRuntimeError(
|
||||
f"Attempted to invoke a context {type_name} without context."
|
||||
)
|
||||
|
||||
args.insert(0, context)
|
||||
elif getattr(func, f"evalcontext{mark_name}", False) is True:
|
||||
if eval_ctx is None:
|
||||
if context is not None:
|
||||
eval_ctx = context.eval_ctx
|
||||
else:
|
||||
eval_ctx = EvalContext(self)
|
||||
|
||||
args.insert(0, eval_ctx)
|
||||
elif getattr(func, f"environment{mark_name}", False) is True:
|
||||
args.insert(0, self)
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
def call_filter(
|
||||
self, name, value, args=None, kwargs=None, context=None, eval_ctx=None
|
||||
):
|
||||
"""Invokes a filter on a value the same way the compiler does.
|
||||
"""Invoke a filter on a value the same way the compiler does.
|
||||
|
||||
This might return a coroutine if the filter is running from an
|
||||
environment in async mode and the filter supports async
|
||||
@ -481,36 +518,28 @@ class Environment:
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
func = self.filters.get(name)
|
||||
if func is None:
|
||||
fail_for_missing_callable("filter", name)
|
||||
args = [value] + list(args or ())
|
||||
if getattr(func, "contextfilter", False) is True:
|
||||
if context is None:
|
||||
raise TemplateRuntimeError(
|
||||
"Attempted to invoke context filter without context"
|
||||
)
|
||||
args.insert(0, context)
|
||||
elif getattr(func, "evalcontextfilter", False) is True:
|
||||
if eval_ctx is None:
|
||||
if context is not None:
|
||||
eval_ctx = context.eval_ctx
|
||||
else:
|
||||
eval_ctx = EvalContext(self)
|
||||
args.insert(0, eval_ctx)
|
||||
elif getattr(func, "environmentfilter", False) is True:
|
||||
args.insert(0, self)
|
||||
return func(*args, **(kwargs or {}))
|
||||
return self._filter_test_common(
|
||||
name, value, args, kwargs, context, eval_ctx, True
|
||||
)
|
||||
|
||||
def call_test(self, name, value, args=None, kwargs=None):
|
||||
"""Invokes a test on a value the same way the compiler does it.
|
||||
def call_test(
|
||||
self, name, value, args=None, kwargs=None, context=None, eval_ctx=None
|
||||
):
|
||||
"""Invoke a test on a value the same way the compiler does.
|
||||
|
||||
This might return a coroutine if the test is running from an
|
||||
environment in async mode and the test supports async execution.
|
||||
It's your responsibility to await this if needed.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
Tests support ``@contextfunction``, etc. decorators. Added
|
||||
the ``context`` and ``eval_ctx`` parameters.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
func = self.tests.get(name)
|
||||
if func is None:
|
||||
fail_for_missing_callable("test", name)
|
||||
return func(value, *(args or ()), **(kwargs or {}))
|
||||
return self._filter_test_common(
|
||||
name, value, args, kwargs, context, eval_ctx, False
|
||||
)
|
||||
|
||||
@internalcode
|
||||
def parse(self, source, name=None, filename=None):
|
||||
|
@ -633,71 +633,76 @@ def args_as_const(node, eval_ctx):
|
||||
return args, kwargs
|
||||
|
||||
|
||||
class Filter(Expr):
|
||||
"""This node applies a filter on an expression. `name` is the name of
|
||||
the filter, the rest of the fields are the same as for :class:`Call`.
|
||||
|
||||
If the `node` of a filter is `None` the contents of the last buffer are
|
||||
filtered. Buffers are created by macros and filter blocks.
|
||||
"""
|
||||
|
||||
class _FilterTestCommon(Expr):
|
||||
fields = ("node", "name", "args", "kwargs", "dyn_args", "dyn_kwargs")
|
||||
abstract = True
|
||||
_is_filter = True
|
||||
|
||||
def as_const(self, eval_ctx=None):
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
|
||||
if eval_ctx.volatile or self.node is None:
|
||||
if eval_ctx.volatile:
|
||||
raise Impossible()
|
||||
|
||||
filter_ = self.environment.filters.get(self.name)
|
||||
if self._is_filter:
|
||||
env_map = eval_ctx.environment.filters
|
||||
mark_name = "filter"
|
||||
else:
|
||||
env_map = eval_ctx.environment.tests
|
||||
# Filters use "contextfilter", tests and calls use "contextfunction".
|
||||
mark_name = "function"
|
||||
|
||||
if filter_ is None or getattr(filter_, "contextfilter", False) is True:
|
||||
func = env_map.get(self.name)
|
||||
|
||||
if func is None or getattr(func, f"context{mark_name}", False) is True:
|
||||
raise Impossible()
|
||||
|
||||
# We cannot constant handle async filters, so we need to make
|
||||
# sure to not go down this path. Account for both sync/async and
|
||||
# pure-async filters.
|
||||
if eval_ctx.environment.is_async and (
|
||||
getattr(filter_, "asyncfiltervariant", False)
|
||||
or inspect.iscoroutinefunction(filter_)
|
||||
getattr(func, f"async{mark_name}variant", False)
|
||||
or inspect.iscoroutinefunction(func)
|
||||
):
|
||||
raise Impossible()
|
||||
|
||||
args, kwargs = args_as_const(self, eval_ctx)
|
||||
args.insert(0, self.node.as_const(eval_ctx))
|
||||
|
||||
if getattr(filter_, "evalcontextfilter", False) is True:
|
||||
if getattr(func, f"evalcontext{mark_name}", False) is True:
|
||||
args.insert(0, eval_ctx)
|
||||
elif getattr(filter_, "environmentfilter", False) is True:
|
||||
args.insert(0, self.environment)
|
||||
elif getattr(func, f"environment{mark_name}", False) is True:
|
||||
args.insert(0, eval_ctx.environment)
|
||||
|
||||
try:
|
||||
return filter_(*args, **kwargs)
|
||||
return func(*args, **kwargs)
|
||||
except Exception:
|
||||
raise Impossible()
|
||||
|
||||
|
||||
class Test(Expr):
|
||||
"""Applies a test on an expression. `name` is the name of the test, the
|
||||
rest of the fields are the same as for :class:`Call`.
|
||||
class Filter(_FilterTestCommon):
|
||||
"""Apply a filter to an expression. ``name`` is the name of the
|
||||
filter, the other fields are the same as :class:`Call`.
|
||||
|
||||
If ``node`` is ``None``, the filter is being used in a filter block
|
||||
and is applied to the content of the block.
|
||||
"""
|
||||
|
||||
fields = ("node", "name", "args", "kwargs", "dyn_args", "dyn_kwargs")
|
||||
|
||||
def as_const(self, eval_ctx=None):
|
||||
test = self.environment.tests.get(self.name)
|
||||
|
||||
if test is None:
|
||||
if self.node is None:
|
||||
raise Impossible()
|
||||
|
||||
eval_ctx = get_eval_context(self, eval_ctx)
|
||||
args, kwargs = args_as_const(self, eval_ctx)
|
||||
args.insert(0, self.node.as_const(eval_ctx))
|
||||
return super().as_const(eval_ctx=eval_ctx)
|
||||
|
||||
try:
|
||||
return test(*args, **kwargs)
|
||||
except Exception:
|
||||
raise Impossible()
|
||||
|
||||
class Test(_FilterTestCommon):
|
||||
"""Apply a test to an expression. ``name`` is the name of the test,
|
||||
the other field are the same as :class:`Call`.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
``as_const`` shares the same logic for filters and tests. Tests
|
||||
check for volatile, async, and ``@contextfunction`` etc.
|
||||
decorators.
|
||||
"""
|
||||
|
||||
_is_filter = False
|
||||
|
||||
|
||||
class Call(Expr):
|
||||
|
@ -5,6 +5,7 @@ from collections import abc
|
||||
from numbers import Number
|
||||
|
||||
from .runtime import Undefined
|
||||
from .utils import environmentfunction
|
||||
|
||||
number_re = re.compile(r"^-?\d+(\.\d+)?$")
|
||||
regex_type = type(number_re)
|
||||
@ -48,6 +49,46 @@ def test_undefined(value):
|
||||
return isinstance(value, Undefined)
|
||||
|
||||
|
||||
@environmentfunction
|
||||
def test_filter(env, value):
|
||||
"""Check if a filter exists by name. Useful if a filter may be
|
||||
optionally available.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{% if 'markdown' is filter %}
|
||||
{{ value | markdown }}
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return value in env.filters
|
||||
|
||||
|
||||
@environmentfunction
|
||||
def test_test(env, value):
|
||||
"""Check if a test exists by name. Useful if a test may be
|
||||
optionally available.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{% if 'loud' is test %}
|
||||
{% if value is loud %}
|
||||
{{ value|upper }}
|
||||
{% else %}
|
||||
{{ value|lower }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return value in env.tests
|
||||
|
||||
|
||||
def test_none(value):
|
||||
"""Return true if the variable is none."""
|
||||
return value is None
|
||||
@ -176,6 +217,8 @@ TESTS = {
|
||||
"divisibleby": test_divisibleby,
|
||||
"defined": test_defined,
|
||||
"undefined": test_undefined,
|
||||
"filter": test_filter,
|
||||
"test": test_test,
|
||||
"none": test_none,
|
||||
"boolean": test_boolean,
|
||||
"false": test_false,
|
||||
|
@ -778,13 +778,13 @@ class TestFilter:
|
||||
assert result == "Hello!\nThis is Jinja saying\nsomething."
|
||||
|
||||
def test_filter_undefined(self, env):
|
||||
with pytest.raises(TemplateAssertionError, match="no filter named 'f'"):
|
||||
with pytest.raises(TemplateAssertionError, match="No filter named 'f'"):
|
||||
env.from_string("{{ var|f }}")
|
||||
|
||||
def test_filter_undefined_in_if(self, env):
|
||||
t = env.from_string("{%- if x is defined -%}{{ x|f }}{%- else -%}x{% endif %}")
|
||||
assert t.render() == "x"
|
||||
with pytest.raises(TemplateRuntimeError, match="no filter named 'f'"):
|
||||
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
|
||||
t.render(x=42)
|
||||
|
||||
def test_filter_undefined_in_elif(self, env):
|
||||
@ -793,7 +793,7 @@ class TestFilter:
|
||||
"{{ y|f }}{%- else -%}foo{%- endif -%}"
|
||||
)
|
||||
assert t.render() == "foo"
|
||||
with pytest.raises(TemplateRuntimeError, match="no filter named 'f'"):
|
||||
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
|
||||
t.render(y=42)
|
||||
|
||||
def test_filter_undefined_in_else(self, env):
|
||||
@ -801,7 +801,7 @@ class TestFilter:
|
||||
"{%- if x is not defined -%}foo{%- else -%}{{ x|f }}{%- endif -%}"
|
||||
)
|
||||
assert t.render() == "foo"
|
||||
with pytest.raises(TemplateRuntimeError, match="no filter named 'f'"):
|
||||
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
|
||||
t.render(x=42)
|
||||
|
||||
def test_filter_undefined_in_nested_if(self, env):
|
||||
@ -811,7 +811,7 @@ class TestFilter:
|
||||
)
|
||||
assert t.render() == "foo"
|
||||
assert t.render(x=42) == "42"
|
||||
with pytest.raises(TemplateRuntimeError, match="no filter named 'f'"):
|
||||
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
|
||||
t.render(x=24, y=42)
|
||||
|
||||
def test_filter_undefined_in_condexpr(self, env):
|
||||
@ -819,6 +819,6 @@ class TestFilter:
|
||||
t2 = env.from_string("{{ 'foo' if x is not defined else x|f }}")
|
||||
assert t1.render() == t2.render() == "foo"
|
||||
|
||||
with pytest.raises(TemplateRuntimeError, match="no filter named 'f'"):
|
||||
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
|
||||
t1.render(x=42)
|
||||
t2.render(x=42)
|
||||
|
@ -2,6 +2,8 @@ import pytest
|
||||
|
||||
from jinja2 import Environment
|
||||
from jinja2 import Markup
|
||||
from jinja2 import TemplateAssertionError
|
||||
from jinja2 import TemplateRuntimeError
|
||||
|
||||
|
||||
class MyDict(dict):
|
||||
@ -206,3 +208,26 @@ class TestTestsCase:
|
||||
'{{ "baz" is in {"bar": 1}}}'
|
||||
)
|
||||
assert tmpl.render() == "True|True|False|True|False|True|False|True|False"
|
||||
|
||||
|
||||
def test_name_undefined(env):
|
||||
with pytest.raises(TemplateAssertionError, match="No test named 'f'"):
|
||||
env.from_string("{{ x is f }}")
|
||||
|
||||
|
||||
def test_name_undefined_in_if(env):
|
||||
t = env.from_string("{% if x is defined %}{{ x is f }}{% endif %}")
|
||||
assert t.render() == ""
|
||||
|
||||
with pytest.raises(TemplateRuntimeError, match="No test named 'f'"):
|
||||
t.render(x=1)
|
||||
|
||||
|
||||
def test_is_filter(env):
|
||||
assert env.call_test("filter", "title")
|
||||
assert not env.call_test("filter", "bad-name")
|
||||
|
||||
|
||||
def test_is_test(env):
|
||||
assert env.call_test("test", "number")
|
||||
assert not env.call_test("test", "bad-name")
|
||||
|
Loading…
x
Reference in New Issue
Block a user