]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
unify/rename filter and function decorators 1389/head
authorDavid Lord <davidism@gmail.com>
Sat, 10 Apr 2021 15:58:16 +0000 (08:58 -0700)
committerDavid Lord <davidism@gmail.com>
Sat, 10 Apr 2021 17:14:42 +0000 (10:14 -0700)
Use pass_context instead of contextfilter and contextfunction, etc.

17 files changed:
CHANGES.rst
docs/api.rst
docs/faq.rst
src/jinja2/__init__.py
src/jinja2/asyncfilters.py
src/jinja2/compiler.py
src/jinja2/environment.py
src/jinja2/ext.py
src/jinja2/filters.py
src/jinja2/nodes.py
src/jinja2/runtime.py
src/jinja2/tests.py
src/jinja2/utils.py
tests/test_api.py
tests/test_ext.py
tests/test_regression.py
tests/test_runtime.py

index c94894ac63c73b87764d7d308f47a4dd3a78d50f..1c58908c0ff643d4e91948285429bcc49ce67434 100644 (file)
@@ -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
index a01239379f8cc5c7dcc26a80aa9bf436a39c6294..ce3a26eda1026af4178ee70162b4dd17be7433be 100644 (file)
@@ -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`.
 
 
@@ -786,44 +792,53 @@ being filtered the second argument.
 Evaluation Context
 ------------------
 
-The evaluation context (short eval context or eval ctx) is a new object
-introduced in Jinja 2.4 that makes it possible to activate and deactivate
-compiled features at runtime.
+The evaluation context (short eval context or eval ctx) makes it
+possible to activate and deactivate compiled features at runtime.
 
-Currently it is only used to enable and disable the automatic escaping but
-can be used for extensions as well.
+Currently it is only used to enable and disable automatic escaping, but
+it can be used by 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 ``autoescape`` setting should be checked on 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
index 1e29e12f9b9432892b8e27861e5f03080da5c977..dd782175074e42851601fe598bc919b8ce5989fc 100644 (file)
@@ -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::
index 8fa05183e5423c06fb2a9fe971aef60d6618db07..2682304b165ea8f6c6eddb39c7faa9db949e8fe1 100644 (file)
@@ -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"
index dfd8cba0526ddd850df6583442e8f060d6ac8a81..00cae01bb9a6e6fbcd2d0845313d878dc9600264 100644 (file)
@@ -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,
index 7a15d8074d9544eb4610fc5747c6745b2dfd5a2e..1d73f7d43ec7f2e06221c9bf92f1dbf64f8bc264 100644 (file)
@@ -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.
index 6211340a383578fab47300654ee77ca0509a4f84..2a64a0ab19389e77cab1110915c89eea8e3da73b 100644 (file)
@@ -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
index 0b2b441d04333dfe878a8390aedf7dc95b79bd1e..cbcf8f30e09a23f062618ee842b5b5aba07a053d 100644 (file)
@@ -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)
index c925f837ac588626d8dbcf5cfcecbf089fdd1784..82f2ff2137be57700ae16df83e888eda09eb893c 100644 (file)
@@ -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,59 @@ 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":
-    """Decorator for marking context dependent filters. The current
-    :class:`Context` will be passed as first argument.
+def contextfilter(f):
+    """Pass the context as the first argument to the decorated function.
+
+    .. deprecated:: 3.0.0
+        Use :func:`~jinja2.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):
+    """Pass the eval context as the first argument to the decorated
+    function.
 
-def evalcontextfilter(f: "F") -> "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 :func:`~jinja2.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):
+    """Pass the environment as the first argument to the decorated
+    function.
 
-def environmentfilter(f: "F") -> "F":
-    """Decorator for marking environment dependent filters.  The current
-    :class:`Environment` is passed to the filter as first argument.
+    .. deprecated:: 3.0.0
+        Use :func:`~jinja2.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 +216,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 +262,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 +367,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 +423,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 +476,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 +496,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 +549,7 @@ def do_default(
     return value
 
 
-@evalcontextfilter
+@pass_eval_context
 def do_join(
     eval_ctx: "EvalContext",
     value: t.Iterable,
@@ -587,7 +612,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 +623,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 +642,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 +692,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 +820,7 @@ def do_indent(
     return rv
 
 
-@environmentfilter
+@pass_environment
 def do_truncate(
     env: "Environment",
     s: str,
@@ -843,7 +868,7 @@ def do_truncate(
     return result + end
 
 
-@environmentfilter
+@pass_environment
 def do_wordwrap(
     environment: "Environment",
     s: str,
@@ -1114,7 +1139,7 @@ class _GroupTuple(t.NamedTuple):
         return tuple.__str__(self)
 
 
-@environmentfilter
+@pass_environment
 def do_groupby(
     environment: "Environment",
     value: "t.Iterable[V]",
@@ -1173,7 +1198,7 @@ def do_groupby(
     ]
 
 
-@environmentfilter
+@pass_environment
 def do_sum(
     environment: "Environment",
     iterable: "t.Iterable[V]",
@@ -1247,7 +1272,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 +1321,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 +1369,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 +1400,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 +1426,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 +1456,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 +1484,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:
index 49e56d29925f51447708ab5839a3e34541cd26a0..bbe1ab757ecc26d15c028d0a64ec8cd4a741fd65 100644 (file)
@@ -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.
     """
 
 
index c3d6fa4cc9fe542bf6b30c120ae167223ef9b056..3d55819408a5c06029ec46971353b60dea3ef33f 100644 (file)
@@ -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
index 229f16a98ccd282631a41679cfaed6fb034a7ee0..a467cf08b54879ee734617611aef72ed946d4566 100644 (file)
@@ -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.
index 842410a28585b486e262e992fd9962b5ed11a35d..61505780f31707c55564cbbfa66d04a46c064901 100644 (file)
@@ -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.runtime.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:`~jinja2.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:`~jinja2.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:`~jinja2.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):
index 5b21bcc55a9b02b0c8c169542c32e14664b3d486..eda04a9be8dd29c378118300f0540d3c4404bacc 100644 (file)
@@ -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)
index 9790f9517e60f7bab06e333a02cfcae5fe852b49..20b19d8b64f07d22e6fdf3e4a82f0f21f48c4d9a 100644 (file)
@@ -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")
 
index 29caee52e7dfaa43e89b34a2e6d16946c10149d4..8e86e41806c5c4d44a4b9b2f9e051e07c257391e 100644 (file)
@@ -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']}"
 
index db95899d9d331176e39e6d79279003438193aaf2..1978c64104a89072e1b936bb0a69a9d14fbc1251 100644 (file)
@@ -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: