summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
10 files changed, 281 insertions, 150 deletions
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):