diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/jinja2/__init__.py | 3 | ||||
-rw-r--r-- | src/jinja2/asyncfilters.py | 66 | ||||
-rw-r--r-- | src/jinja2/compiler.py | 47 | ||||
-rw-r--r-- | src/jinja2/environment.py | 14 | ||||
-rw-r--r-- | src/jinja2/ext.py | 12 | ||||
-rw-r--r-- | src/jinja2/filters.py | 91 | ||||
-rw-r--r-- | src/jinja2/nodes.py | 22 | ||||
-rw-r--r-- | src/jinja2/runtime.py | 34 | ||||
-rw-r--r-- | src/jinja2/tests.py | 6 | ||||
-rw-r--r-- | src/jinja2/utils.py | 136 |
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): |