summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Lord <davidism@gmail.com>2021-04-10 08:58:16 -0700
committerDavid Lord <davidism@gmail.com>2021-04-10 08:58:55 -0700
commitc2a36c2b0d4f869e9104338695d9dc12e26832d6 (patch)
tree61c80e550fc6ba1e7e68753169bccbf1df580d48
parentc8db6c6313e90a47da92722f60010a6ada2e8644 (diff)
downloadjinja2-unify-decorators.tar.gz
unify/rename filter and function decoratorsunify-decorators
Use pass_context instead of contextfilter and contextfunction, etc.
-rw-r--r--CHANGES.rst13
-rw-r--r--docs/api.rst68
-rw-r--r--docs/faq.rst12
-rw-r--r--src/jinja2/__init__.py3
-rw-r--r--src/jinja2/asyncfilters.py66
-rw-r--r--src/jinja2/compiler.py47
-rw-r--r--src/jinja2/environment.py14
-rw-r--r--src/jinja2/ext.py12
-rw-r--r--src/jinja2/filters.py91
-rw-r--r--src/jinja2/nodes.py22
-rw-r--r--src/jinja2/runtime.py34
-rw-r--r--src/jinja2/tests.py6
-rw-r--r--src/jinja2/utils.py136
-rw-r--r--tests/test_api.py12
-rw-r--r--tests/test_ext.py10
-rw-r--r--tests/test_regression.py24
-rw-r--r--tests/test_runtime.py4
17 files changed, 364 insertions, 210 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index c94894a..1c58908 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -44,8 +44,8 @@ Unreleased
- 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`
+ decorated with ``@pass_environment``, ``@pass_eval_context``,
+ or ``@pass_context``. :issue:`842`, :pr:`1248`
- Support ``pgettext`` and ``npgettext`` (message contexts) in i18n
extension. :issue:`441`
- The ``|indent`` filter's ``width`` argument can be a string to
@@ -60,6 +60,15 @@ Unreleased
breaks. Other characters are left unchanged. :issue:`769, 952, 1313`
- ``|groupby`` filter takes an optional ``default`` argument.
:issue:`1359`
+- The function and filter decorators have been renamed and unified.
+ The old names are deprecated. :issue:`1381`
+
+ - ``pass_context`` replaces ``contextfunction`` and
+ ``contextfilter``.
+ - ``pass_eval_context`` replaces ``evalcontextfunction`` and
+ ``evalcontextfilter``
+ - ``pass_environment`` replaces ``environmentfunction`` and
+ ``environmentfilter``.
Version 2.11.3
diff --git a/docs/api.rst b/docs/api.rst
index a012393..8f36f2e 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -591,18 +591,24 @@ Utilities
These helper functions and classes are useful if you add custom filters or
functions to a Jinja environment.
-.. autofunction:: jinja2.environmentfilter
+.. autofunction:: jinja2.pass_context
+
+.. autofunction:: jinja2.pass_eval_context
+
+.. autofunction:: jinja2.pass_environment
.. autofunction:: jinja2.contextfilter
.. autofunction:: jinja2.evalcontextfilter
-.. autofunction:: jinja2.environmentfunction
+.. autofunction:: jinja2.environmentfilter
.. autofunction:: jinja2.contextfunction
.. autofunction:: jinja2.evalcontextfunction
+.. autofunction:: jinja2.environmentfunction
+
.. function:: escape(s)
Convert the characters ``&``, ``<``, ``>``, ``'``, and ``"`` in string `s`
@@ -697,9 +703,9 @@ 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:`environmentfilter` passes the :class:`Environment`.
-- :func:`evalcontextfilter` passes the :ref:`eval-context`.
-- :func:`contextfilter` passes the current
+- :func:`pass_environment` passes the :class:`Environment`.
+- :func:`pass_eval_context` passes the :ref:`eval-context`.
+- :func:`pass_context` passes the current
:class:`~jinja2.runtime.Context`.
Here's a filter that converts line breaks into HTML ``<br>`` and ``<p>``
@@ -709,10 +715,10 @@ enabled before escaping the input and marking the output safe.
.. code-block:: python
import re
- from jinja2 import evalcontextfilter
+ from jinja2 import pass_eval_context
from markupsafe import Markup, escape
- @evalcontextfilter
+ @pass_eval_context
def nl2br(eval_ctx, value):
br = "<br>\n"
@@ -775,9 +781,9 @@ 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
+- :func:`pass_environment` passes the :class:`Environment`.
+- :func:`pass_eval_context` passes the :ref:`eval-context`.
+- :func:`pass_context` passes the current
:class:`~jinja2.runtime.Context`.
@@ -793,37 +799,47 @@ compiled features at runtime.
Currently it is only used to enable and disable the automatic escaping but
can be used for extensions as well.
-In previous Jinja versions filters and functions were marked as
-environment callables in order to check for the autoescape status from the
-environment. In new versions it's encouraged to check the setting from the
-evaluation context instead.
+The setting should be checked the evaluation context, not the
+environment. The evaluation context will have the computed value for the
+current template.
-Previous versions::
+Instead of ``pass_environment``:
- @environmentfilter
+.. code-block:: python
+
+ @pass_environment
def filter(env, value):
result = do_something(value)
+
if env.autoescape:
result = Markup(result)
+
return result
-In new versions you can either use a :func:`contextfilter` and access the
-evaluation context from the actual context, or use a
-:func:`evalcontextfilter` which directly passes the evaluation context to
-the function::
+Use ``pass_eval_context`` if you only need the setting:
- @contextfilter
- def filter(context, value):
+.. code-block:: python
+
+ @pass_eval_context
+ def filter(eval_ctx, value):
result = do_something(value)
- if context.eval_ctx.autoescape:
+
+ if eval_ctx.autoescape:
result = Markup(result)
+
return result
- @evalcontextfilter
- def filter(eval_ctx, value):
+Or use ``pass_context`` if you need other context behavior as well:
+
+.. code-block:: python
+
+ @pass_context
+ def filter(context, value):
result = do_something(value)
- if eval_ctx.autoescape:
+
+ if context.eval_ctx.autoescape:
result = Markup(result)
+
return result
The evaluation context must not be modified at runtime. Modifications
diff --git a/docs/faq.rst b/docs/faq.rst
index 1e29e12..dd78217 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -113,12 +113,12 @@ CSS, JavaScript, or configuration files.
Why is the Context immutable?
-----------------------------
-When writing a :func:`contextfunction` or something similar you may have
-noticed that the context tries to stop you from modifying it. If you have
-managed to modify the context by using an internal context API you may
-have noticed that changes in the context don't seem to be visible in the
-template. The reason for this is that Jinja uses the context only as
-primary data source for template variables for performance reasons.
+When writing a :func:`pass_context` function, you may have noticed that
+the context tries to stop you from modifying it. If you have managed to
+modify the context by using an internal context API you may have noticed
+that changes in the context don't seem to be visible in the template.
+The reason for this is that Jinja uses the context only as primary data
+source for template variables for performance reasons.
If you want to modify the context write a function that returns a variable
instead that one can assign to a variable by using set::
diff --git a/src/jinja2/__init__.py b/src/jinja2/__init__.py
index 8fa0518..2682304 100644
--- a/src/jinja2/__init__.py
+++ b/src/jinja2/__init__.py
@@ -38,6 +38,9 @@ from .utils import contextfunction
from .utils import environmentfunction
from .utils import evalcontextfunction
from .utils import is_undefined
+from .utils import pass_context
+from .utils import pass_environment
+from .utils import pass_eval_context
from .utils import select_autoescape
__version__ = "3.0.0a1"
diff --git a/src/jinja2/asyncfilters.py b/src/jinja2/asyncfilters.py
index dfd8cba..00cae01 100644
--- a/src/jinja2/asyncfilters.py
+++ b/src/jinja2/asyncfilters.py
@@ -1,11 +1,14 @@
import typing
import typing as t
+import warnings
from functools import wraps
from itertools import groupby
from . import filters
from .asyncsupport import auto_aiter
from .asyncsupport import auto_await
+from .utils import _PassArg
+from .utils import pass_eval_context
if t.TYPE_CHECKING:
from .environment import Environment
@@ -49,50 +52,59 @@ async def async_select_or_reject(
yield item
-def dualfilter(normal_filter, async_filter):
- wrap_evalctx = False
+def dual_filter(normal_func, async_func):
+ pass_arg = _PassArg.from_obj(normal_func)
+ wrapper_has_eval_context = False
- if getattr(normal_filter, "environmentfilter", False) is True:
+ if pass_arg is _PassArg.environment:
+ wrapper_has_eval_context = False
def is_async(args):
return args[0].is_async
- wrap_evalctx = False
else:
- has_evalctxfilter = getattr(normal_filter, "evalcontextfilter", False) is True
- has_ctxfilter = getattr(normal_filter, "contextfilter", False) is True
- wrap_evalctx = not has_evalctxfilter and not has_ctxfilter
+ wrapper_has_eval_context = pass_arg is None
def is_async(args):
return args[0].environment.is_async
- @wraps(normal_filter)
+ @wraps(normal_func)
def wrapper(*args, **kwargs):
b = is_async(args)
- if wrap_evalctx:
+ if wrapper_has_eval_context:
args = args[1:]
if b:
- return async_filter(*args, **kwargs)
+ return async_func(*args, **kwargs)
- return normal_filter(*args, **kwargs)
+ return normal_func(*args, **kwargs)
- if wrap_evalctx:
- wrapper.evalcontextfilter = True
+ if wrapper_has_eval_context:
+ wrapper = pass_eval_context(wrapper)
- wrapper.asyncfiltervariant = True
+ wrapper.jinja_async_variant = True
return wrapper
-def asyncfiltervariant(original):
+def async_variant(original):
def decorator(f):
- return dualfilter(original, f)
+ return dual_filter(original, f)
return decorator
-@asyncfiltervariant(filters.do_first)
+def asyncfiltervariant(original):
+ warnings.warn(
+ "'asyncfiltervariant' is renamed to 'async_variant', the old"
+ " name will be removed in Jinja 3.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return async_variant(original)
+
+
+@async_variant(filters.do_first)
async def do_first(
environment: "Environment", seq: "t.Union[t.AsyncIterable[V], t.Iterable[V]]"
) -> "t.Union[V, Undefined]":
@@ -102,7 +114,7 @@ async def do_first(
return environment.undefined("No first item, sequence was empty.")
-@asyncfiltervariant(filters.do_groupby)
+@async_variant(filters.do_groupby)
async def do_groupby(
environment: "Environment",
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
@@ -116,7 +128,7 @@ async def do_groupby(
]
-@asyncfiltervariant(filters.do_join)
+@async_variant(filters.do_join)
async def do_join(
eval_ctx: "EvalContext",
value: t.Union[t.AsyncIterable, t.Iterable],
@@ -126,12 +138,12 @@ async def do_join(
return filters.do_join(eval_ctx, await auto_to_seq(value), d, attribute)
-@asyncfiltervariant(filters.do_list)
+@async_variant(filters.do_list)
async def do_list(value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]") -> "t.List[V]":
return await auto_to_seq(value)
-@asyncfiltervariant(filters.do_reject)
+@async_variant(filters.do_reject)
async def do_reject(
context: "Context",
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
@@ -141,7 +153,7 @@ async def do_reject(
return async_select_or_reject(context, value, args, kwargs, lambda x: not x, False)
-@asyncfiltervariant(filters.do_rejectattr)
+@async_variant(filters.do_rejectattr)
async def do_rejectattr(
context: "Context",
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
@@ -151,7 +163,7 @@ async def do_rejectattr(
return async_select_or_reject(context, value, args, kwargs, lambda x: not x, True)
-@asyncfiltervariant(filters.do_select)
+@async_variant(filters.do_select)
async def do_select(
context: "Context",
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
@@ -161,7 +173,7 @@ async def do_select(
return async_select_or_reject(context, value, args, kwargs, lambda x: x, False)
-@asyncfiltervariant(filters.do_selectattr)
+@async_variant(filters.do_selectattr)
async def do_selectattr(
context: "Context",
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
@@ -193,7 +205,7 @@ def do_map(
...
-@asyncfiltervariant(filters.do_map)
+@async_variant(filters.do_map)
async def do_map(context, value, *args, **kwargs):
if value:
func = filters.prepare_map(context, args, kwargs)
@@ -202,7 +214,7 @@ async def do_map(context, value, *args, **kwargs):
yield await auto_await(func(item))
-@asyncfiltervariant(filters.do_sum)
+@async_variant(filters.do_sum)
async def do_sum(
environment: "Environment",
iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
@@ -224,7 +236,7 @@ async def do_sum(
return rv
-@asyncfiltervariant(filters.do_slice)
+@async_variant(filters.do_slice)
async def do_slice(
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
slices: int,
diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py
index 7a15d80..1d73f7d 100644
--- a/src/jinja2/compiler.py
+++ b/src/jinja2/compiler.py
@@ -19,6 +19,7 @@ from .idtracking import VAR_LOAD_RESOLVE
from .idtracking import VAR_LOAD_UNDEFINED
from .nodes import EvalContext
from .optimizer import Optimizer
+from .utils import _PassArg
from .utils import concat
from .visitor import NodeVisitor
@@ -1282,21 +1283,25 @@ class CodeGenerator(NodeVisitor):
if self.environment.finalize:
src = "environment.finalize("
env_finalize = self.environment.finalize
+ pass_arg = {
+ _PassArg.context: "context",
+ _PassArg.eval_context: "context.eval_ctx",
+ _PassArg.environment: "environment",
+ }.get(_PassArg.from_obj(env_finalize))
+ finalize = None
- def finalize(value):
- return default(env_finalize(value))
-
- if getattr(env_finalize, "contextfunction", False) is True:
- src += "context, "
- finalize = None # noqa: F811
- elif getattr(env_finalize, "evalcontextfunction", False) is True:
- src += "context.eval_ctx, "
- finalize = None
- elif getattr(env_finalize, "environmentfunction", False) is True:
- src += "environment, "
+ if pass_arg is None:
def finalize(value):
- return default(env_finalize(self.environment, value))
+ return default(env_finalize(value))
+
+ else:
+ src = f"{src}{pass_arg}, "
+
+ if pass_arg == "environment":
+
+ def finalize(value):
+ return default(env_finalize(self.environment, value))
self._finalize = self._FinalizeInfo(finalize, src)
return self._finalize
@@ -1666,13 +1671,11 @@ class CodeGenerator(NodeVisitor):
if is_filter:
compiler_map = self.filters
env_map = self.environment.filters
- type_name = mark_name = "filter"
+ type_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(")
@@ -1686,12 +1689,14 @@ class CodeGenerator(NodeVisitor):
if func is None and not frame.soft_frame:
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, f"evalcontext{mark_name}", False) is True:
- self.write("context.eval_ctx, ")
- elif getattr(func, f"environment{mark_name}", False) is True:
- self.write("environment, ")
+ pass_arg = {
+ _PassArg.context: "context",
+ _PassArg.eval_context: "context.eval_ctx",
+ _PassArg.environment: "environment",
+ }.get(_PassArg.from_obj(func))
+
+ if pass_arg is not None:
+ self.write(f"{pass_arg}, ")
# Back to the visitor function to handle visiting the target of
# the filter or test.
diff --git a/src/jinja2/environment.py b/src/jinja2/environment.py
index 6211340..2a64a0a 100644
--- a/src/jinja2/environment.py
+++ b/src/jinja2/environment.py
@@ -42,6 +42,7 @@ from .parser import Parser
from .runtime import Context
from .runtime import new_context
from .runtime import Undefined
+from .utils import _PassArg
from .utils import concat
from .utils import consume
from .utils import have_async_gen
@@ -464,12 +465,10 @@ class Environment:
):
if is_filter:
env_map = self.filters
- type_name = mark_name = "filter"
+ type_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)
@@ -486,15 +485,16 @@ class Environment:
args = [value, *(args if args is not None else ())]
kwargs = kwargs if kwargs is not None else {}
+ pass_arg = _PassArg.from_obj(func)
- if getattr(func, f"context{mark_name}", False) is True:
+ if pass_arg is _PassArg.context:
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:
+ elif pass_arg is _PassArg.eval_context:
if eval_ctx is None:
if context is not None:
eval_ctx = context.eval_ctx
@@ -502,7 +502,7 @@ class Environment:
eval_ctx = EvalContext(self)
args.insert(0, eval_ctx)
- elif getattr(func, f"environment{mark_name}", False) is True:
+ elif pass_arg is _PassArg.environment:
args.insert(0, self)
return func(*args, **kwargs)
@@ -532,7 +532,7 @@ class Environment:
It's your responsibility to await this if needed.
.. versionchanged:: 3.0
- Tests support ``@contextfunction``, etc. decorators. Added
+ Tests support ``@pass_context``, etc. decorators. Added
the ``context`` and ``eval_ctx`` parameters.
.. versionadded:: 2.7
diff --git a/src/jinja2/ext.py b/src/jinja2/ext.py
index 0b2b441..cbcf8f3 100644
--- a/src/jinja2/ext.py
+++ b/src/jinja2/ext.py
@@ -25,8 +25,8 @@ from .exceptions import TemplateAssertionError
from .exceptions import TemplateSyntaxError
from .nodes import ContextReference
from .runtime import concat
-from .utils import contextfunction
from .utils import import_string
+from .utils import pass_context
# I18N functions available in Jinja templates. If the I18N library
# provides ugettext, it will be assigned to gettext.
@@ -135,13 +135,13 @@ class Extension(metaclass=ExtensionRegistry):
)
-@contextfunction
+@pass_context
def _gettext_alias(__context, *args, **kwargs):
return __context.call(__context.resolve("gettext"), *args, **kwargs)
def _make_new_gettext(func):
- @contextfunction
+ @pass_context
def gettext(__context, __string, **variables):
rv = __context.call(func, __string)
if __context.eval_ctx.autoescape:
@@ -155,7 +155,7 @@ def _make_new_gettext(func):
def _make_new_ngettext(func):
- @contextfunction
+ @pass_context
def ngettext(__context, __singular, __plural, __num, **variables):
variables.setdefault("num", __num)
rv = __context.call(func, __singular, __plural, __num)
@@ -168,7 +168,7 @@ def _make_new_ngettext(func):
def _make_new_pgettext(func):
- @contextfunction
+ @pass_context
def pgettext(__context, __string_ctx, __string, **variables):
variables.setdefault("context", __string_ctx)
rv = __context.call(func, __string_ctx, __string)
@@ -183,7 +183,7 @@ def _make_new_pgettext(func):
def _make_new_npgettext(func):
- @contextfunction
+ @pass_context
def npgettext(__context, __string_ctx, __singular, __plural, __num, **variables):
variables.setdefault("context", __string_ctx)
variables.setdefault("num", __num)
diff --git a/src/jinja2/filters.py b/src/jinja2/filters.py
index c925f83..2db25e6 100644
--- a/src/jinja2/filters.py
+++ b/src/jinja2/filters.py
@@ -4,6 +4,7 @@ import random
import re
import typing
import typing as t
+import warnings
from collections import abc
from itertools import chain
from itertools import groupby
@@ -15,6 +16,9 @@ from markupsafe import soft_str
from .exceptions import FilterArgumentError
from .runtime import Undefined
from .utils import htmlsafe_json_dumps
+from .utils import pass_context
+from .utils import pass_environment
+from .utils import pass_eval_context
from .utils import pformat
from .utils import url_quote
from .utils import urlize
@@ -28,38 +32,61 @@ if t.TYPE_CHECKING:
K = t.TypeVar("K")
V = t.TypeVar("V")
- F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class HasHTML(te.Protocol):
def __html__(self) -> str:
pass
-def contextfilter(f: "F") -> "F":
+def contextfilter(f):
"""Decorator for marking context dependent filters. The current
:class:`Context` will be passed as first argument.
+
+ .. deprecated:: 3.0.0
+ Use :attr:`jinja2.utils.pass_context` instead.
"""
- f.contextfilter = True # type: ignore
- return f
+ warnings.warn(
+ "'contextfilter' is renamed to 'pass_context', the old name"
+ " will be removed in Jinja 3.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return pass_context(f)
-def evalcontextfilter(f: "F") -> "F":
+def evalcontextfilter(f):
"""Decorator for marking eval-context dependent filters. An eval
context object is passed as first argument. For more information
about the eval context, see :ref:`eval-context`.
+ .. deprecated:: 3.0.0
+ Use :attr:`jinja2.utils.pass_eval_context` instead.
+
.. versionadded:: 2.4
"""
- f.evalcontextfilter = True # type: ignore
- return f
+ warnings.warn(
+ "'evalcontextfilter' is renamed to 'pass_eval_context', the old"
+ " name will be removed in Jinja 3.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return pass_eval_context(f)
-def environmentfilter(f: "F") -> "F":
+def environmentfilter(f):
"""Decorator for marking environment dependent filters. The current
:class:`Environment` is passed to the filter as first argument.
+
+ .. deprecated:: 3.0.0
+ Use :attr:`jinja2.utils.pass_environment` instead.
"""
- f.environmentfilter = True # type: ignore
- return f
+ warnings.warn(
+ "'environmentfilter' is renamed to 'pass_environment', the old"
+ " name will be removed in Jinja 3.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return pass_environment(f)
def ignore_case(value: "V") -> "V":
@@ -191,7 +218,7 @@ def do_urlencode(
)
-@evalcontextfilter
+@pass_eval_context
def do_replace(
eval_ctx: "EvalContext", s: str, old: str, new: str, count: t.Optional[int] = None
) -> str:
@@ -237,7 +264,7 @@ def do_lower(s: str) -> str:
return soft_str(s).lower()
-@evalcontextfilter
+@pass_eval_context
def do_xmlattr(
eval_ctx: "EvalContext", d: t.Mapping[str, t.Any], autospace: bool = True
) -> str:
@@ -342,7 +369,7 @@ def do_dictsort(
return sorted(value.items(), key=sort_func, reverse=reverse)
-@environmentfilter
+@pass_environment
def do_sort(
environment: "Environment",
value: "t.Iterable[V]",
@@ -398,7 +425,7 @@ def do_sort(
return sorted(value, key=key_func, reverse=reverse)
-@environmentfilter
+@pass_environment
def do_unique(
environment: "Environment",
value: "t.Iterable[V]",
@@ -451,7 +478,7 @@ def _min_or_max(
return func(chain([first], it), key=key_func)
-@environmentfilter
+@pass_environment
def do_min(
environment: "Environment",
value: "t.Iterable[V]",
@@ -471,7 +498,7 @@ def do_min(
return _min_or_max(environment, value, min, case_sensitive, attribute)
-@environmentfilter
+@pass_environment
def do_max(
environment: "Environment",
value: "t.Iterable[V]",
@@ -524,7 +551,7 @@ def do_default(
return value
-@evalcontextfilter
+@pass_eval_context
def do_join(
eval_ctx: "EvalContext",
value: t.Iterable,
@@ -587,7 +614,7 @@ def do_center(value: str, width: int = 80) -> str:
return soft_str(value).center(width)
-@environmentfilter
+@pass_environment
def do_first(
environment: "Environment", seq: "t.Iterable[V]"
) -> "t.Union[V, Undefined]":
@@ -598,7 +625,7 @@ def do_first(
return environment.undefined("No first item, sequence was empty.")
-@environmentfilter
+@pass_environment
def do_last(
environment: "Environment", seq: "t.Reversible[V]"
) -> "t.Union[V, Undefined]":
@@ -617,7 +644,7 @@ def do_last(
return environment.undefined("No last item, sequence was empty.")
-@contextfilter
+@pass_context
def do_random(context: "Context", seq: "t.Sequence[V]") -> "t.Union[V, Undefined]":
"""Return a random item from the sequence."""
try:
@@ -667,7 +694,7 @@ def do_pprint(value: t.Any) -> str:
_uri_scheme_re = re.compile(r"^([\w.+-]{2,}:(/){0,2})$")
-@evalcontextfilter
+@pass_eval_context
def do_urlize(
eval_ctx: "EvalContext",
value: str,
@@ -795,7 +822,7 @@ def do_indent(
return rv
-@environmentfilter
+@pass_environment
def do_truncate(
env: "Environment",
s: str,
@@ -843,7 +870,7 @@ def do_truncate(
return result + end
-@environmentfilter
+@pass_environment
def do_wordwrap(
environment: "Environment",
s: str,
@@ -1114,7 +1141,7 @@ class _GroupTuple(t.NamedTuple):
return tuple.__str__(self)
-@environmentfilter
+@pass_environment
def do_groupby(
environment: "Environment",
value: "t.Iterable[V]",
@@ -1173,7 +1200,7 @@ def do_groupby(
]
-@environmentfilter
+@pass_environment
def do_sum(
environment: "Environment",
iterable: "t.Iterable[V]",
@@ -1247,7 +1274,7 @@ def do_reverse(value):
raise FilterArgumentError("argument must be iterable")
-@environmentfilter
+@pass_environment
def do_attr(
environment: "Environment", obj: t.Any, name: str
) -> t.Union[Undefined, t.Any]:
@@ -1296,7 +1323,7 @@ def do_map(
...
-@contextfilter
+@pass_context
def do_map(context, value, *args, **kwargs):
"""Applies a filter on a sequence of objects or looks up an attribute.
This is useful when dealing with lists of objects but you are really
@@ -1344,7 +1371,7 @@ def do_map(context, value, *args, **kwargs):
yield func(item)
-@contextfilter
+@pass_context
def do_select(
context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any
) -> "t.Iterator[V]":
@@ -1375,7 +1402,7 @@ def do_select(
return select_or_reject(context, value, args, kwargs, lambda x: x, False)
-@contextfilter
+@pass_context
def do_reject(
context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any
) -> "t.Iterator[V]":
@@ -1401,7 +1428,7 @@ def do_reject(
return select_or_reject(context, value, args, kwargs, lambda x: not x, False)
-@contextfilter
+@pass_context
def do_selectattr(
context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any
) -> "t.Iterator[V]":
@@ -1431,7 +1458,7 @@ def do_selectattr(
return select_or_reject(context, value, args, kwargs, lambda x: x, True)
-@contextfilter
+@pass_context
def do_rejectattr(
context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any
) -> "t.Iterator[V]":
@@ -1459,7 +1486,7 @@ def do_rejectattr(
return select_or_reject(context, value, args, kwargs, lambda x: not x, True)
-@evalcontextfilter
+@pass_eval_context
def do_tojson(
eval_ctx: "EvalContext", value: t.Any, indent: t.Optional[int] = None
) -> Markup:
diff --git a/src/jinja2/nodes.py b/src/jinja2/nodes.py
index 49e56d2..bbe1ab7 100644
--- a/src/jinja2/nodes.py
+++ b/src/jinja2/nodes.py
@@ -10,6 +10,8 @@ from typing import Tuple as TupleType
from markupsafe import Markup
+from .utils import _PassArg
+
_binop_to_func = {
"*": operator.mul,
"/": operator.truediv,
@@ -646,19 +648,17 @@ class _FilterTestCommon(Expr):
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"
func = env_map.get(self.name)
+ pass_arg = _PassArg.from_obj(func)
- if func is None or getattr(func, f"context{mark_name}", False) is True:
+ if func is None or pass_arg is _PassArg.context:
raise Impossible()
if eval_ctx.environment.is_async and (
- getattr(func, f"async{mark_name}variant", False)
+ getattr(func, "jinja_async_variant", False) is True
or inspect.iscoroutinefunction(func)
):
raise Impossible()
@@ -666,9 +666,9 @@ class _FilterTestCommon(Expr):
args, kwargs = args_as_const(self, eval_ctx)
args.insert(0, self.node.as_const(eval_ctx))
- if getattr(func, f"evalcontext{mark_name}", False) is True:
+ if pass_arg is _PassArg.eval_context:
args.insert(0, eval_ctx)
- elif getattr(func, f"environment{mark_name}", False) is True:
+ elif pass_arg is _PassArg.environment:
args.insert(0, eval_ctx.environment)
try:
@@ -698,7 +698,7 @@ class Test(_FilterTestCommon):
.. versionchanged:: 3.0
``as_const`` shares the same logic for filters and tests. Tests
- check for volatile, async, and ``@contextfunction`` etc.
+ check for volatile, async, and ``@pass_context`` etc.
decorators.
"""
@@ -990,9 +990,9 @@ class ContextReference(Expr):
Getattr(ContextReference(), 'name'))
This is basically equivalent to using the
- :func:`~jinja2.contextfunction` decorator when using the
- high-level API, which causes a reference to the context to be passed
- as the first argument to a function.
+ :func:`~jinja2.pass_context` decorator when using the high-level
+ API, which causes a reference to the context to be passed as the
+ first argument to a function.
"""
diff --git a/src/jinja2/runtime.py b/src/jinja2/runtime.py
index c3d6fa4..3d55819 100644
--- a/src/jinja2/runtime.py
+++ b/src/jinja2/runtime.py
@@ -13,12 +13,13 @@ from .exceptions import TemplateNotFound # noqa: F401
from .exceptions import TemplateRuntimeError # noqa: F401
from .exceptions import UndefinedError
from .nodes import EvalContext
+from .utils import _PassArg
from .utils import concat
-from .utils import evalcontextfunction
from .utils import internalcode
from .utils import missing
from .utils import Namespace # noqa: F401
from .utils import object_type_repr
+from .utils import pass_eval_context
if t.TYPE_CHECKING:
from .environment import Environment
@@ -171,7 +172,7 @@ class Context(metaclass=ContextMeta):
The context is immutable. Modifications on :attr:`parent` **must not**
happen and modifications on :attr:`vars` are allowed from generated
template code only. Template filters and global functions marked as
- :func:`contextfunction`\\s get the active context passed as first argument
+ :func:`pass_context` get the active context passed as first argument
and are allowed to access the context read-only.
The template context supports read only dict operations (`get`,
@@ -268,26 +269,23 @@ class Context(metaclass=ContextMeta):
def call(__self, __obj, *args, **kwargs): # noqa: B902
"""Call the callable with the arguments and keyword arguments
provided but inject the active context or environment as first
- argument if the callable is a :func:`contextfunction` or
- :func:`environmentfunction`.
+ argument if the callable has :func:`pass_context` or
+ :func:`pass_environment`.
"""
if __debug__:
__traceback_hide__ = True # noqa
# Allow callable classes to take a context
- if hasattr(__obj, "__call__"): # noqa: B004
- fn = __obj.__call__
- for fn_type in (
- "contextfunction",
- "evalcontextfunction",
- "environmentfunction",
- ):
- if hasattr(fn, fn_type):
- __obj = fn
- break
+ if (
+ hasattr(__obj, "__call__") # noqa: B004
+ and _PassArg.from_obj(__obj.__call__) is not None
+ ):
+ __obj = __obj.__call__
if callable(__obj):
- if getattr(__obj, "contextfunction", False) is True:
+ pass_arg = _PassArg.from_obj(__obj)
+
+ if pass_arg is _PassArg.context:
# the active context should have access to variables set in
# loops and blocks without mutating the context itself
if kwargs.get("_loop_vars"):
@@ -295,9 +293,9 @@ class Context(metaclass=ContextMeta):
if kwargs.get("_block_vars"):
__self = __self.derived(kwargs["_block_vars"])
args = (__self,) + args
- elif getattr(__obj, "evalcontextfunction", False) is True:
+ elif pass_arg is _PassArg.eval_context:
args = (__self.eval_ctx,) + args
- elif getattr(__obj, "environmentfunction", False) is True:
+ elif pass_arg is _PassArg.environment:
args = (__self.environment,) + args
kwargs.pop("_block_vars", None)
@@ -597,7 +595,7 @@ class Macro:
self._default_autoescape = default_autoescape
@internalcode
- @evalcontextfunction
+ @pass_eval_context
def __call__(self, *args, **kwargs):
# This requires a bit of explanation, In the past we used to
# decide largely based on compile-time information if a macro is
diff --git a/src/jinja2/tests.py b/src/jinja2/tests.py
index 229f16a..a467cf0 100644
--- a/src/jinja2/tests.py
+++ b/src/jinja2/tests.py
@@ -5,7 +5,7 @@ from collections import abc
from numbers import Number
from .runtime import Undefined
-from .utils import environmentfunction
+from .utils import pass_environment
if t.TYPE_CHECKING:
from .environment import Environment
@@ -48,7 +48,7 @@ def test_undefined(value: t.Any) -> bool:
return isinstance(value, Undefined)
-@environmentfunction
+@pass_environment
def test_filter(env: "Environment", value: str) -> bool:
"""Check if a filter exists by name. Useful if a filter may be
optionally available.
@@ -66,7 +66,7 @@ def test_filter(env: "Environment", value: str) -> bool:
return value in env.filters
-@environmentfunction
+@pass_environment
def test_test(env: "Environment", value: str) -> bool:
"""Check if a test exists by name. Useful if a test may be
optionally available.
diff --git a/src/jinja2/utils.py b/src/jinja2/utils.py
index 842410a..abbd44b 100644
--- a/src/jinja2/utils.py
+++ b/src/jinja2/utils.py
@@ -1,7 +1,9 @@
+import enum
import json
import os
import re
import typing as t
+import warnings
from collections import abc
from collections import deque
from random import choice
@@ -13,6 +15,9 @@ from urllib.parse import quote_from_bytes
from markupsafe import escape
from markupsafe import Markup
+if t.TYPE_CHECKING:
+ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
+
# special singleton representing missing values for the runtime
missing = type("MissingType", (), {"__repr__": lambda x: "missing"})()
@@ -24,43 +29,124 @@ concat = "".join
_slash_escape = "\\/" not in json.dumps("/")
-def contextfunction(f):
- """This decorator can be used to mark a function or method context callable.
- A context callable is passed the active :class:`Context` as first argument when
- called from the template. This is useful if a function wants to get access
- to the context or functions provided on the context object. For example
- a function that returns a sorted list of template variables the current
- template exports could look like this::
-
- @contextfunction
- def get_exported_names(context):
- return sorted(context.exported_vars)
+def pass_context(f: "F") -> "F":
+ """Pass the :class:`~jinja2.Context`` as the first argument to the
+ decorated function when called while rendering a template.
+
+ Can be used on functions, filters, and tests.
+
+ If only ``Context.eval_context`` is needed, use
+ :func:`pass_eval_context`. If only ``Context.environment`` is
+ needed, use :func:`pass_environment`.
+
+ .. versionadded:: 3.0.0
+ Replaces ``contextfunction`` and ``contextfilter``.
+ """
+ f.jinja_pass_arg = _PassArg.context # type: ignore
+ return f
+
+
+def pass_eval_context(f: "F") -> "F":
+ """Pass the :class:`~jinja2.nodes.EvalContext`` as the first
+ argument to the decorated function when called while rendering a
+ template. See :ref:`eval-context`.
+
+ Can be used on functions, filters, and tests.
+
+ If only ``EvalContext.environment`` is needed, use
+ :func:`pass_environment`.
+
+ .. versionadded:: 3.0.0
+ Replaces ``evalcontextfunction`` and ``evalcontextfilter``.
"""
- f.contextfunction = True
+ f.jinja_pass_arg = _PassArg.eval_context # type: ignore
return f
+def pass_environment(f: "F") -> "F":
+ """Pass the :class:`~jinja2.Environment`` as the first argument to
+ the decorated function when called while rendering a template.
+
+ Can be used on functions, filters, and tests.
+
+ .. versionadded:: 3.0.0
+ Replaces ``environmentfunction`` and ``environmentfilter``.
+ """
+ f.jinja_pass_arg = _PassArg.environment # type: ignore
+ return f
+
+
+class _PassArg(enum.Enum):
+ context = enum.auto()
+ eval_context = enum.auto()
+ environment = enum.auto()
+
+ @classmethod
+ def from_obj(cls, obj):
+ if hasattr(obj, "jinja_pass_arg"):
+ return obj.jinja_pass_arg
+
+ for prefix in "context", "eval_context", "environment":
+ squashed = prefix.replace("_", "")
+
+ for name in f"{squashed}function", f"{squashed}filter":
+ if getattr(obj, name, False) is True:
+ warnings.warn(
+ f"{name!r} is deprecated and will stop working"
+ f" in Jinja 3.1. Use 'pass_{prefix}' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return cls[prefix]
+
+
+def contextfunction(f):
+ """Pass the context as the first argument to the decorated function.
+
+ .. deprecated:: 3.0.0
+ Use :func:`pass_context` instead.
+ """
+ warnings.warn(
+ "'contextfunction' is renamed to 'pass_context', the old name"
+ " will be removed in Jinja 3.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return pass_context(f)
+
+
def evalcontextfunction(f):
- """This decorator can be used to mark a function or method as an eval
- context callable. This is similar to the :func:`contextfunction`
- but instead of passing the context, an evaluation context object is
- passed. For more information about the eval context, see
- :ref:`eval-context`.
+ """Pass the eval context as the first argument to the decorated
+ function.
+
+ .. deprecated:: 3.0.0
+ Use :func:`pass_eval_context` instead.
.. versionadded:: 2.4
"""
- f.evalcontextfunction = True
- return f
+ warnings.warn(
+ "'evalcontextfunction' is renamed to 'pass_eval_context', the"
+ " old name will be removed in Jinja 3.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return pass_eval_context(f)
def environmentfunction(f):
- """This decorator can be used to mark a function or method as environment
- callable. This decorator works exactly like the :func:`contextfunction`
- decorator just that the first argument is the active :class:`Environment`
- and not context.
+ """Pass the environment as the first argument to the decorated
+ function.
+
+ .. deprecated:: 3.0.0
+ Use :func:`pass_environment` instead.
"""
- f.environmentfunction = True
- return f
+ warnings.warn(
+ "'environmentfunction' is renamed to 'pass_environment', the"
+ " old name will be removed in Jinja 3.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return pass_environment(f)
def internalcode(f):
diff --git a/tests/test_api.py b/tests/test_api.py
index 5b21bcc..eda04a9 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -18,10 +18,10 @@ from jinja2 import Undefined
from jinja2 import UndefinedError
from jinja2.compiler import CodeGenerator
from jinja2.runtime import Context
-from jinja2.utils import contextfunction
from jinja2.utils import Cycler
-from jinja2.utils import environmentfunction
-from jinja2.utils import evalcontextfunction
+from jinja2.utils import pass_context
+from jinja2.utils import pass_environment
+from jinja2.utils import pass_eval_context
class TestExtendedAPI:
@@ -53,7 +53,7 @@ class TestExtendedAPI:
assert t.render(value=123) == "<int>"
def test_context_finalize(self):
- @contextfunction
+ @pass_context
def finalize(context, value):
return value * context["scale"]
@@ -62,7 +62,7 @@ class TestExtendedAPI:
assert t.render(value=5, scale=3) == "15"
def test_eval_finalize(self):
- @evalcontextfunction
+ @pass_eval_context
def finalize(eval_ctx, value):
return str(eval_ctx.autoescape) + value
@@ -71,7 +71,7 @@ class TestExtendedAPI:
assert t.render(value="<script>") == "True&lt;script&gt;"
def test_env_autoescape(self):
- @environmentfunction
+ @pass_environment
def finalize(env, value):
return " ".join(
(env.variable_start_string, repr(value), env.variable_end_string)
diff --git a/tests/test_ext.py b/tests/test_ext.py
index 9790f95..20b19d8 100644
--- a/tests/test_ext.py
+++ b/tests/test_ext.py
@@ -3,10 +3,10 @@ from io import BytesIO
import pytest
-from jinja2 import contextfunction
from jinja2 import DictLoader
from jinja2 import Environment
from jinja2 import nodes
+from jinja2 import pass_context
from jinja2.exceptions import TemplateAssertionError
from jinja2.ext import Extension
from jinja2.lexer import count_newlines
@@ -74,14 +74,14 @@ def _get_with_context(value, ctx=None):
return value
-@contextfunction
+@pass_context
def gettext(context, string):
language = context.get("LANGUAGE", "en")
value = languages.get(language, {}).get(string, string)
return _get_with_context(value)
-@contextfunction
+@pass_context
def ngettext(context, s, p, n):
language = context.get("LANGUAGE", "en")
@@ -93,14 +93,14 @@ def ngettext(context, s, p, n):
return _get_with_context(value)
-@contextfunction
+@pass_context
def pgettext(context, c, s):
language = context.get("LANGUAGE", "en")
value = languages.get(language, {}).get(s, s)
return _get_with_context(value, c)
-@contextfunction
+@pass_context
def npgettext(context, c, s, p, n):
language = context.get("LANGUAGE", "en")
diff --git a/tests/test_regression.py b/tests/test_regression.py
index 29caee5..8e86e41 100644
--- a/tests/test_regression.py
+++ b/tests/test_regression.py
@@ -7,7 +7,7 @@ from jinja2 import Template
from jinja2 import TemplateAssertionError
from jinja2 import TemplateNotFound
from jinja2 import TemplateSyntaxError
-from jinja2.utils import contextfunction
+from jinja2.utils import pass_context
class TestCorner:
@@ -298,11 +298,9 @@ class TestBug:
assert e.value.name == "foo/bar.html"
- def test_contextfunction_callable_classes(self, env):
- from jinja2.utils import contextfunction
-
+ def test_pass_context_callable_class(self, env):
class CallableClass:
- @contextfunction
+ @pass_context
def __call__(self, ctx):
return ctx.resolve("hello")
@@ -633,8 +631,8 @@ End"""
)
assert tmpl.render() == "Start\n1) foo\n2) bar last\nEnd"
- def test_contextfunction_loop_vars(self, env):
- @contextfunction
+ def test_pass_context_loop_vars(self, env):
+ @pass_context
def test(ctx):
return f"{ctx['i']}{ctx['j']}"
@@ -653,8 +651,8 @@ End"""
tmpl.globals["test"] = test
assert tmpl.render() == "42\n01\n01\n42\n12\n12\n42"
- def test_contextfunction_scoped_loop_vars(self, env):
- @contextfunction
+ def test_pass_context_scoped_loop_vars(self, env):
+ @pass_context
def test(ctx):
return f"{ctx['i']}"
@@ -673,8 +671,8 @@ End"""
tmpl.globals["test"] = test
assert tmpl.render() == "42\n0\n42\n1\n42"
- def test_contextfunction_in_blocks(self, env):
- @contextfunction
+ def test_pass_context_in_blocks(self, env):
+ @pass_context
def test(ctx):
return f"{ctx['i']}"
@@ -691,8 +689,8 @@ End"""
tmpl.globals["test"] = test
assert tmpl.render() == "42\n24\n42"
- def test_contextfunction_block_and_loop(self, env):
- @contextfunction
+ def test_pass_context_block_and_loop(self, env):
+ @pass_context
def test(ctx):
return f"{ctx['i']}"
diff --git a/tests/test_runtime.py b/tests/test_runtime.py
index db95899..1978c64 100644
--- a/tests/test_runtime.py
+++ b/tests/test_runtime.py
@@ -56,10 +56,10 @@ def test_iterator_not_advanced_early():
assert out == "1 [(1, 'a'), (1, 'b')]\n2 [(2, 'c')]\n3 [(3, 'd')]\n"
-def test_mock_not_contextfunction():
+def test_mock_not_pass_arg_marker():
"""If a callable class has a ``__getattr__`` that returns True-like
values for arbitrary attrs, it should not be incorrectly identified
- as a ``contextfunction``.
+ as a ``pass_context`` function.
"""
class Calc: