diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 09a67888f..838e0a21c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.11.0 + rev: v2.12.0 hooks: - id: pyupgrade args: ["--py36-plus"] diff --git a/CHANGES.rst b/CHANGES.rst index d162b560c..ac3f55726 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -72,6 +72,12 @@ Unreleased - ``pass_environment`` replaces ``environmentfunction`` and ``environmentfilter``. +- Async support no longer requires Jinja to patch itself. It must + still be enabled with ``Environment(enable_async=True)``. + :issue:`1390` +- Overriding ``Context.resolve`` is deprecated, override + ``resolve_or_missing`` instead. :issue:`1380` + Version 2.11.3 -------------- diff --git a/docs/api.rst b/docs/api.rst index c83aa1334..6e87aae35 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -531,7 +531,7 @@ The Context ----------- .. autoclass:: jinja2.runtime.Context() - :members: resolve, get_exported, get_all + :members: get, resolve, resolve_or_missing, get_exported, get_all .. attribute:: parent diff --git a/docs/conf.py b/docs/conf.py index 7fcb96ca4..7b302b3b8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,6 +18,7 @@ "sphinxcontrib.log_cabinet", "sphinx_issues", ] +autodoc_typehints = "description" intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "markupsafe": ("https://markupsafe.palletsprojects.com/", None), diff --git a/docs/intro.rst b/docs/intro.rst index d043f7f98..264dd8ffc 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -12,8 +12,8 @@ It includes: - HTML templates can use autoescaping to prevent XSS from untrusted user input. - A sandboxed environment can safely render untrusted templates. -- AsyncIO support for generating templates and calling async - functions. +- Async support for generating templates that automatically handle + sync and async functions without extra syntax. - I18N support with Babel. - Templates are compiled to optimized Python code just-in-time and cached, or can be compiled ahead-of-time. diff --git a/docs/templates.rst b/docs/templates.rst index cb0186851..8a7f20180 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -1,8 +1,9 @@ +.. py:currentmodule:: jinja2 +.. highlight:: html+jinja + Template Designer Documentation =============================== -.. highlight:: html+jinja - This document describes the syntax and semantics of the template engine and will be most useful as reference to those creating Jinja templates. As the template engine is very flexible, the configuration from the application can @@ -1485,6 +1486,8 @@ is a bit contrived in the context of rendering a template): List of Builtin Filters ----------------------- +.. py:currentmodule:: jinja-filters + .. jinja:filters:: jinja2.defaults.DEFAULT_FILTERS @@ -1493,6 +1496,8 @@ List of Builtin Filters List of Builtin Tests --------------------- +.. py:currentmodule:: jinja-tests + .. jinja:tests:: jinja2.defaults.DEFAULT_TESTS @@ -1503,6 +1508,8 @@ List of Global Functions The following functions are available in the global scope by default: +.. py:currentmodule:: jinja-globals + .. function:: range([start,] stop[, step]) Return a list containing an arithmetic progression of integers. @@ -1626,6 +1633,8 @@ The following functions are available in the global scope by default: Extensions ---------- +.. py:currentmodule:: jinja2 + The following sections cover the built-in Jinja extensions that may be enabled by an application. An application could also provide further extensions not covered by this documentation; in which case there should diff --git a/requirements/dev.txt b/requirements/dev.txt index 4bc39d737..e47b869fb 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile requirements/dev.in +# pip-compile ../requirements/dev.in # alabaster==0.7.12 # via sphinx @@ -28,7 +28,7 @@ filelock==3.0.12 # via # tox # virtualenv -identify==1.5.13 +identify==2.2.3 # via pre-commit idna==2.10 # via requests @@ -42,9 +42,9 @@ markupsafe==1.1.1 # via jinja2 mypy-extensions==0.4.3 # via mypy -mypy==0.800 - # via -r requirements/typing.in -nodeenv==1.5.0 +mypy==0.812 + # via -r ../requirements/typing.in +nodeenv==1.6.0 # via pre-commit packaging==20.9 # via @@ -53,25 +53,27 @@ packaging==20.9 # sphinx # tox pallets-sphinx-themes==1.2.3 - # via -r requirements/docs.in -pip-tools==5.5.0 - # via -r requirements/dev.in + # via -r ../requirements/docs.in +pep517==0.10.0 + # via pip-tools +pip-tools==6.0.1 + # via -r ../requirements/dev.in pluggy==0.13.1 # via # pytest # tox -pre-commit==2.10.1 - # via -r requirements/dev.in +pre-commit==2.12.0 + # via -r ../requirements/dev.in py==1.10.0 # via # pytest # tox -pygments==2.7.4 +pygments==2.8.1 # via sphinx pyparsing==2.4.7 # via packaging -pytest==6.2.2 - # via -r requirements/tests.in +pytest==6.2.3 + # via -r ../requirements/tests.in pytz==2021.1 # via babel pyyaml==5.4.1 @@ -85,10 +87,10 @@ six==1.15.0 snowballstemmer==2.1.0 # via sphinx sphinx-issues==1.2.0 - # via -r requirements/docs.in -sphinx==2.4.4 + # via -r ../requirements/docs.in +sphinx==3.5.4 # via - # -r requirements/docs.in + # -r ../requirements/docs.in # pallets-sphinx-themes # sphinx-issues # sphinxcontrib-log-cabinet @@ -101,25 +103,26 @@ sphinxcontrib-htmlhelp==1.0.3 sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-log-cabinet==1.0.1 - # via -r requirements/docs.in + # via -r ../requirements/docs.in sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.4 # via sphinx toml==0.10.2 # via + # pep517 # pre-commit # pytest # tox -tox==3.21.4 - # via -r requirements/dev.in -typed-ast==1.4.2 +tox==3.23.0 + # via -r ../requirements/dev.in +typed-ast==1.4.3 # via mypy typing-extensions==3.7.4.3 # via mypy urllib3==1.26.4 # via requests -virtualenv==20.4.2 +virtualenv==20.4.3 # via # pre-commit # tox diff --git a/requirements/docs.in b/requirements/docs.in index 42f165117..7ec501b6d 100644 --- a/requirements/docs.in +++ b/requirements/docs.in @@ -1,4 +1,4 @@ Pallets-Sphinx-Themes -Sphinx<3 +Sphinx sphinx-issues sphinxcontrib-log-cabinet diff --git a/requirements/docs.txt b/requirements/docs.txt index 2465b6140..604c17513 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile requirements/docs.in +# pip-compile ../requirements/docs.in # alabaster==0.7.12 # via sphinx @@ -27,8 +27,8 @@ packaging==20.9 # pallets-sphinx-themes # sphinx pallets-sphinx-themes==1.2.3 - # via -r requirements/docs.in -pygments==2.7.4 + # via -r ../requirements/docs.in +pygments==2.8.1 # via sphinx pyparsing==2.4.7 # via packaging @@ -39,10 +39,10 @@ requests==2.25.1 snowballstemmer==2.1.0 # via sphinx sphinx-issues==1.2.0 - # via -r requirements/docs.in -sphinx==2.4.4 + # via -r ../requirements/docs.in +sphinx==3.5.4 # via - # -r requirements/docs.in + # -r ../requirements/docs.in # pallets-sphinx-themes # sphinx-issues # sphinxcontrib-log-cabinet @@ -55,7 +55,7 @@ sphinxcontrib-htmlhelp==1.0.3 sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-log-cabinet==1.0.1 - # via -r requirements/docs.in + # via -r ../requirements/docs.in sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.4 diff --git a/requirements/tests.txt b/requirements/tests.txt index 43d6aba60..fbf52fef1 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile requirements/tests.in +# pip-compile ../requirements/tests.in # attrs==20.3.0 # via pytest @@ -16,7 +16,7 @@ py==1.10.0 # via pytest pyparsing==2.4.7 # via packaging -pytest==6.2.2 - # via -r requirements/tests.in +pytest==6.2.3 + # via -r ../requirements/tests.in toml==0.10.2 # via pytest diff --git a/requirements/typing.txt b/requirements/typing.txt index 2530301a5..383fc8f4f 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -2,13 +2,13 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile requirements/typing.in +# pip-compile ../requirements/typing.in # mypy-extensions==0.4.3 # via mypy -mypy==0.800 - # via -r requirements/typing.in -typed-ast==1.4.2 +mypy==0.812 + # via -r ../requirements/typing.in +typed-ast==1.4.3 # via mypy typing-extensions==3.7.4.3 # via mypy diff --git a/src/jinja2/async_utils.py b/src/jinja2/async_utils.py new file mode 100644 index 000000000..cb011b246 --- /dev/null +++ b/src/jinja2/async_utils.py @@ -0,0 +1,76 @@ +import inspect +import typing as t +from functools import wraps + +from .utils import _PassArg +from .utils import pass_eval_context + +if t.TYPE_CHECKING: + V = t.TypeVar("V") + + +def async_variant(normal_func): + def decorator(async_func): + pass_arg = _PassArg.from_obj(normal_func) + need_eval_context = pass_arg is None + + if pass_arg is _PassArg.environment: + + def is_async(args): + return args[0].is_async + + else: + + def is_async(args): + return args[0].environment.is_async + + @wraps(normal_func) + def wrapper(*args, **kwargs): + b = is_async(args) + + if need_eval_context: + args = args[1:] + + if b: + return async_func(*args, **kwargs) + + return normal_func(*args, **kwargs) + + if need_eval_context: + wrapper = pass_eval_context(wrapper) + + wrapper.jinja_async_variant = True + return wrapper + + return decorator + + +async def auto_await(value): + if inspect.isawaitable(value): + return await value + + return value + + +async def auto_aiter(iterable): + if hasattr(iterable, "__aiter__"): + async for item in iterable: + yield item + else: + for item in iterable: + yield item + + +async def auto_to_list( + value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", +) -> "t.List[V]": + seq = [] + + if hasattr(value, "__aiter__"): + async for item in t.cast(t.AsyncIterable, value): + seq.append(item) + else: + for item in t.cast(t.Iterable, value): + seq.append(item) + + return seq diff --git a/src/jinja2/asyncfilters.py b/src/jinja2/asyncfilters.py deleted file mode 100644 index 00cae01bb..000000000 --- a/src/jinja2/asyncfilters.py +++ /dev/null @@ -1,261 +0,0 @@ -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 - from .nodes import EvalContext - from .runtime import Context - from .runtime import Undefined - - V = t.TypeVar("V") - - -async def auto_to_seq( - value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", -) -> "t.List[V]": - seq = [] - - if hasattr(value, "__aiter__"): - async for item in t.cast(t.AsyncIterable, value): - seq.append(item) - else: - for item in t.cast(t.Iterable, value): - seq.append(item) - - return seq - - -async def async_select_or_reject( - context: "Context", - value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", - args: t.Tuple, - kwargs: t.Dict[str, t.Any], - modfunc: t.Callable[[t.Any], t.Any], - lookup_attr: bool, -) -> "t.AsyncIterator[V]": - if value: - func = filters.prepare_select_or_reject( - context, args, kwargs, modfunc, lookup_attr - ) - - async for item in auto_aiter(value): - if func(item): - yield item - - -def dual_filter(normal_func, async_func): - pass_arg = _PassArg.from_obj(normal_func) - wrapper_has_eval_context = False - - if pass_arg is _PassArg.environment: - wrapper_has_eval_context = False - - def is_async(args): - return args[0].is_async - - else: - wrapper_has_eval_context = pass_arg is None - - def is_async(args): - return args[0].environment.is_async - - @wraps(normal_func) - def wrapper(*args, **kwargs): - b = is_async(args) - - if wrapper_has_eval_context: - args = args[1:] - - if b: - return async_func(*args, **kwargs) - - return normal_func(*args, **kwargs) - - if wrapper_has_eval_context: - wrapper = pass_eval_context(wrapper) - - wrapper.jinja_async_variant = True - return wrapper - - -def async_variant(original): - def decorator(f): - return dual_filter(original, f) - - return decorator - - -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]": - try: - return t.cast("V", await auto_aiter(seq).__anext__()) - except StopAsyncIteration: - return environment.undefined("No first item, sequence was empty.") - - -@async_variant(filters.do_groupby) -async def do_groupby( - environment: "Environment", - value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", - attribute: t.Union[str, int], - default: t.Optional[t.Any] = None, -) -> "t.List[t.Tuple[t.Any, t.List[V]]]": - expr = filters.make_attrgetter(environment, attribute, default=default) - return [ - filters._GroupTuple(key, await auto_to_seq(values)) - for key, values in groupby(sorted(await auto_to_seq(value), key=expr), expr) - ] - - -@async_variant(filters.do_join) -async def do_join( - eval_ctx: "EvalContext", - value: t.Union[t.AsyncIterable, t.Iterable], - d: str = "", - attribute: t.Optional[t.Union[str, int]] = None, -) -> str: - return filters.do_join(eval_ctx, await auto_to_seq(value), d, attribute) - - -@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) - - -@async_variant(filters.do_reject) -async def do_reject( - context: "Context", - value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", - *args: t.Any, - **kwargs: t.Any, -) -> "t.AsyncIterator[V]": - return async_select_or_reject(context, value, args, kwargs, lambda x: not x, False) - - -@async_variant(filters.do_rejectattr) -async def do_rejectattr( - context: "Context", - value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", - *args: t.Any, - **kwargs: t.Any, -) -> "t.AsyncIterator[V]": - return async_select_or_reject(context, value, args, kwargs, lambda x: not x, True) - - -@async_variant(filters.do_select) -async def do_select( - context: "Context", - value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", - *args: t.Any, - **kwargs: t.Any, -) -> "t.AsyncIterator[V]": - return async_select_or_reject(context, value, args, kwargs, lambda x: x, False) - - -@async_variant(filters.do_selectattr) -async def do_selectattr( - context: "Context", - value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", - *args: t.Any, - **kwargs: t.Any, -) -> "t.AsyncIterator[V]": - return async_select_or_reject(context, value, args, kwargs, lambda x: x, True) - - -@typing.overload -def do_map( - context: "Context", - value: t.Union[t.AsyncIterable, t.Iterable], - name: str, - *args: t.Any, - **kwargs: t.Any, -) -> t.Iterable: - ... - - -@typing.overload -def do_map( - context: "Context", - value: t.Union[t.AsyncIterable, t.Iterable], - *, - attribute: str = ..., - default: t.Optional[t.Any] = None, -) -> t.Iterable: - ... - - -@async_variant(filters.do_map) -async def do_map(context, value, *args, **kwargs): - if value: - func = filters.prepare_map(context, args, kwargs) - - async for item in auto_aiter(value): - yield await auto_await(func(item)) - - -@async_variant(filters.do_sum) -async def do_sum( - environment: "Environment", - iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", - attribute: t.Optional[t.Union[str, int]] = None, - start: "V" = 0, # type: ignore -) -> "V": - rv = start - - if attribute is not None: - func = filters.make_attrgetter(environment, attribute) - else: - - def func(x): - return x - - async for item in auto_aiter(iterable): - rv += func(item) - - return rv - - -@async_variant(filters.do_slice) -async def do_slice( - value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", - slices: int, - fill_with: t.Optional[t.Any] = None, -) -> "t.Iterator[t.List[V]]": - return filters.do_slice(await auto_to_seq(value), slices, fill_with) - - -ASYNC_FILTERS = { - "first": do_first, - "groupby": do_groupby, - "join": do_join, - "list": do_list, - # we intentionally do not support do_last because it may not be safe in async - "reject": do_reject, - "rejectattr": do_rejectattr, - "map": do_map, - "select": do_select, - "selectattr": do_selectattr, - "sum": do_sum, - "slice": do_slice, -} diff --git a/src/jinja2/asyncsupport.py b/src/jinja2/asyncsupport.py deleted file mode 100644 index d62f3f4d2..000000000 --- a/src/jinja2/asyncsupport.py +++ /dev/null @@ -1,247 +0,0 @@ -"""The code for async support. Importing this patches Jinja.""" -import asyncio -import inspect -from functools import update_wrapper - -from .environment import TemplateModule -from .runtime import LoopContext -from .utils import concat -from .utils import internalcode -from .utils import missing - - -async def concat_async(async_gen): - rv = [] - - async def collect(): - async for event in async_gen: - rv.append(event) - - await collect() - return concat(rv) - - -async def generate_async(self, *args, **kwargs): - vars = dict(*args, **kwargs) - try: - async for event in self.root_render_func(self.new_context(vars)): - yield event - except Exception: - yield self.environment.handle_exception() - - -def wrap_generate_func(original_generate): - def _convert_generator(self, loop, args, kwargs): - async_gen = self.generate_async(*args, **kwargs) - try: - while 1: - yield loop.run_until_complete(async_gen.__anext__()) - except StopAsyncIteration: - pass - - def generate(self, *args, **kwargs): - if not self.environment.is_async: - return original_generate(self, *args, **kwargs) - return _convert_generator(self, asyncio.get_event_loop(), args, kwargs) - - return update_wrapper(generate, original_generate) - - -async def render_async(self, *args, **kwargs): - if not self.environment.is_async: - raise RuntimeError("The environment was not created with async mode enabled.") - - vars = dict(*args, **kwargs) - ctx = self.new_context(vars) - - try: - return await concat_async(self.root_render_func(ctx)) - except Exception: - return self.environment.handle_exception() - - -def wrap_render_func(original_render): - def render(self, *args, **kwargs): - if not self.environment.is_async: - return original_render(self, *args, **kwargs) - loop = asyncio.get_event_loop() - return loop.run_until_complete(self.render_async(*args, **kwargs)) - - return update_wrapper(render, original_render) - - -def wrap_block_reference_call(original_call): - @internalcode - async def async_call(self): - rv = await concat_async(self._stack[self._depth](self._context)) - if self._context.eval_ctx.autoescape: - rv = self._context.eval_ctx.mark_safe(rv) - return rv - - @internalcode - def __call__(self): - if not self._context.environment.is_async: - return original_call(self) - return async_call(self) - - return update_wrapper(__call__, original_call) - - -def wrap_macro_invoke(original_invoke): - @internalcode - async def async_invoke(self, arguments, autoescape): - rv = await self._func(*arguments) - if autoescape: - rv = self._mark_safe(rv) - return rv - - @internalcode - def _invoke(self, arguments, autoescape): - if not self._environment.is_async: - return original_invoke(self, arguments, autoescape) - return async_invoke(self, arguments, autoescape) - - return update_wrapper(_invoke, original_invoke) - - -@internalcode -async def get_default_module_async(self): - if self._module is not None: - return self._module - self._module = rv = await self.make_module_async() - return rv - - -def wrap_default_module(original_default_module): - @internalcode - def _get_default_module(self, ctx=None): - if self.environment.is_async: - raise RuntimeError("Template module attribute is unavailable in async mode") - return original_default_module(self, ctx) - - return _get_default_module - - -async def make_module_async(self, vars=None, shared=False, locals=None): - context = self.new_context(vars, shared, locals) - body_stream = [] - async for item in self.root_render_func(context): - body_stream.append(item) - return TemplateModule(self, context, body_stream) - - -def patch_template(): - from . import Template - - Template.generate = wrap_generate_func(Template.generate) - Template.generate_async = update_wrapper(generate_async, Template.generate_async) - Template.render_async = update_wrapper(render_async, Template.render_async) - Template.render = wrap_render_func(Template.render) - Template._get_default_module = wrap_default_module(Template._get_default_module) - Template._get_default_module_async = get_default_module_async - Template.make_module_async = update_wrapper( - make_module_async, Template.make_module_async - ) - - -def patch_runtime(): - from .runtime import BlockReference, Macro - - BlockReference.__call__ = wrap_block_reference_call(BlockReference.__call__) - Macro._invoke = wrap_macro_invoke(Macro._invoke) - - -def patch_filters(): - from .filters import FILTERS - from .asyncfilters import ASYNC_FILTERS - - FILTERS.update(ASYNC_FILTERS) - - -def patch_all(): - patch_template() - patch_runtime() - patch_filters() - - -async def auto_await(value): - if inspect.isawaitable(value): - return await value - return value - - -async def auto_aiter(iterable): - if hasattr(iterable, "__aiter__"): - async for item in iterable: - yield item - return - for item in iterable: - yield item - - -class AsyncLoopContext(LoopContext): - _to_iterator = staticmethod(auto_aiter) - - @property - async def length(self): - if self._length is not None: - return self._length - - try: - self._length = len(self._iterable) - except TypeError: - iterable = [x async for x in self._iterator] - self._iterator = self._to_iterator(iterable) - self._length = len(iterable) + self.index + (self._after is not missing) - - return self._length - - @property - async def revindex0(self): - return await self.length - self.index - - @property - async def revindex(self): - return await self.length - self.index0 - - async def _peek_next(self): - if self._after is not missing: - return self._after - - try: - self._after = await self._iterator.__anext__() - except StopAsyncIteration: - self._after = missing - - return self._after - - @property - async def last(self): - return await self._peek_next() is missing - - @property - async def nextitem(self): - rv = await self._peek_next() - - if rv is missing: - return self._undefined("there is no next item") - - return rv - - def __aiter__(self): - return self - - async def __anext__(self): - if self._after is not missing: - rv = self._after - self._after = missing - else: - rv = await self._iterator.__anext__() - - self.index0 += 1 - self._before = self._current - self._current = rv - return rv, self - - -patch_all() diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py index 09510fa5a..83b3b192b 100644 --- a/src/jinja2/compiler.py +++ b/src/jinja2/compiler.py @@ -739,16 +739,15 @@ def visit_Template(self, node, frame=None): assert frame is None, "no root frame allowed" eval_ctx = EvalContext(self.environment, self.name) - from .runtime import exported - - self.writeline("from __future__ import generator_stop") # Python < 3.7 - self.writeline("from jinja2.runtime import " + ", ".join(exported)) + from .runtime import exported, async_exported if self.environment.is_async: - self.writeline( - "from jinja2.asyncsupport import auto_await, " - "auto_aiter, AsyncLoopContext" - ) + exported_names = sorted(exported + async_exported) + else: + exported_names = sorted(exported) + + self.writeline("from __future__ import generator_stop") # Python < 3.7 + self.writeline("from jinja2.runtime import " + ", ".join(exported_names)) # if we want a deferred initialization we cannot move the # environment into a local name diff --git a/src/jinja2/environment.py b/src/jinja2/environment.py index d1027e4d8..217982e31 100644 --- a/src/jinja2/environment.py +++ b/src/jinja2/environment.py @@ -49,7 +49,6 @@ from .utils import concat from .utils import consume from .utils import get_wrapped_escape_class -from .utils import have_async_gen from .utils import import_string from .utils import internalcode from .utils import LRUCache @@ -413,12 +412,7 @@ def __init__( # load extensions self.extensions = load_extensions(self, extensions) - self.enable_async = enable_async - self.is_async = self.enable_async and have_async_gen - if self.is_async: - # runs patch_all() to enable async support - from . import asyncsupport # noqa: F401 - + self.is_async = enable_async _environment_sanity_check(self) def get_markup_class( @@ -1293,13 +1287,20 @@ def render(self, *args, **kwargs): This will return the rendered template as a string. """ - vars = dict(*args, **kwargs) + if self.environment.is_async: + import asyncio + + loop = asyncio.get_event_loop() + return loop.run_until_complete(self.render_async(*args, **kwargs)) + + ctx = self.new_context(dict(*args, **kwargs)) + try: - return concat(self.root_render_func(self.new_context(vars))) + return concat(self.root_render_func(ctx)) except Exception: self.environment.handle_exception() - def render_async(self, *args, **kwargs): + async def render_async(self, *args, **kwargs): """This works similar to :meth:`render` but returns a coroutine that when awaited returns the entire rendered template string. This requires the async feature to be enabled. @@ -1308,10 +1309,17 @@ def render_async(self, *args, **kwargs): await template.render_async(knights='that say nih; asynchronously') """ - # see asyncsupport for the actual implementation - raise NotImplementedError( - "This feature is not available for this version of Python" - ) + if not self.environment.is_async: + raise RuntimeError( + "The environment was not created with async mode enabled." + ) + + ctx = self.new_context(dict(*args, **kwargs)) + + try: + return concat([n async for n in self.root_render_func(ctx)]) + except Exception: + return self.environment.handle_exception() def stream(self, *args, **kwargs): """Works exactly like :meth:`generate` but returns a @@ -1327,20 +1335,41 @@ def generate(self, *args, **kwargs): It accepts the same arguments as :meth:`render`. """ - vars = dict(*args, **kwargs) + if self.environment.is_async: + import asyncio + + loop = asyncio.get_event_loop() + async_gen = self.generate_async(*args, **kwargs) + + try: + while True: + yield loop.run_until_complete(async_gen.__anext__()) + except StopAsyncIteration: + return + + ctx = self.new_context(dict(*args, **kwargs)) + try: - yield from self.root_render_func(self.new_context(vars)) + yield from self.root_render_func(ctx) except Exception: yield self.environment.handle_exception() - def generate_async(self, *args, **kwargs): + async def generate_async(self, *args, **kwargs): """An async version of :meth:`generate`. Works very similarly but returns an async iterator instead. """ - # see asyncsupport for the actual implementation - raise NotImplementedError( - "This feature is not available for this version of Python" - ) + if not self.environment.is_async: + raise RuntimeError( + "The environment was not created with async mode enabled." + ) + + ctx = self.new_context(dict(*args, **kwargs)) + + try: + async for event in self.root_render_func(ctx): + yield event + except Exception: + yield self.environment.handle_exception() def new_context(self, vars=None, shared=False, locals=None): """Create a new :class:`Context` for this template. The vars @@ -1361,42 +1390,56 @@ def make_module(self, vars=None, shared=False, locals=None): a dict which is then used as context. The arguments are the same as for the :meth:`new_context` method. """ - return TemplateModule(self, self.new_context(vars, shared, locals)) + ctx = self.new_context(vars, shared, locals) + return TemplateModule(self, ctx) - def make_module_async(self, vars=None, shared=False, locals=None): + async def make_module_async(self, vars=None, shared=False, locals=None): """As template module creation can invoke template code for asynchronous executions this method must be used instead of the normal :meth:`make_module` one. Likewise the module attribute becomes unavailable in async mode. """ - # see asyncsupport for the actual implementation - raise NotImplementedError( - "This feature is not available for this version of Python" - ) + ctx = self.new_context(vars, shared, locals) + return TemplateModule(self, ctx, [x async for x in self.root_render_func(ctx)]) @internalcode def _get_default_module(self, ctx=None): """If a context is passed in, this means that the template was - imported. Imported templates have access to the current template's - globals by default, but they can only be accessed via the context - during runtime. - - If there are new globals, we need to create a new - module because the cached module is already rendered and will not have - access to globals from the current context. This new module is not - cached as :attr:`_module` because the template can be imported elsewhere, - and it should have access to only the current template's globals. + imported. Imported templates have access to the current + template's globals by default, but they can only be accessed via + the context during runtime. + + If there are new globals, we need to create a new module because + the cached module is already rendered and will not have access + to globals from the current context. This new module is not + cached because the template can be imported elsewhere, and it + should have access to only the current template's globals. """ + if self.environment.is_async: + raise RuntimeError("Module is not available in async mode.") + if ctx is not None: - globals = { - key: ctx.parent[key] for key in ctx.globals_keys - self.globals.keys() - } - if globals: - return self.make_module(globals) - if self._module is not None: - return self._module - self._module = rv = self.make_module() - return rv + keys = ctx.globals_keys - self.globals.keys() + + if keys: + return self.make_module({k: ctx.parent[k] for k in keys}) + + if self._module is None: + self._module = self.make_module() + + return self._module + + async def _get_default_module_async(self, ctx=None): + if ctx is not None: + keys = ctx.globals_keys - self.globals.keys() + + if keys: + return await self.make_module_async({k: ctx.parent[k] for k in keys}) + + if self._module is None: + self._module = await self.make_module_async() + + return self._module @property def module(self): diff --git a/src/jinja2/filters.py b/src/jinja2/filters.py index e30124013..9067a34ec 100644 --- a/src/jinja2/filters.py +++ b/src/jinja2/filters.py @@ -11,6 +11,10 @@ import markupsafe +from .async_utils import async_variant +from .async_utils import auto_aiter +from .async_utils import auto_await +from .async_utils import auto_to_list from .exceptions import FilterArgumentError from .runtime import Undefined from .utils import htmlsafe_json_dumps @@ -598,7 +602,7 @@ def do_default( @pass_eval_context -def do_join( +def sync_do_join( eval_ctx: "EvalContext", value: t.Iterable, d: str = "", @@ -658,13 +662,23 @@ def do_join( return markupsafe.soft_str(d).join(map(markupsafe.soft_str, value)) +@async_variant(sync_do_join) +async def do_join( + eval_ctx: "EvalContext", + value: t.Union[t.AsyncIterable, t.Iterable], + d: str = "", + attribute: t.Optional[t.Union[str, int]] = None, +) -> str: + return sync_do_join(eval_ctx, await auto_to_list(value), d, attribute) + + def do_center(value: str, width: int = 80) -> str: """Centers the value in a field of a given width.""" return markupsafe.soft_str(value).center(width) @pass_environment -def do_first( +def sync_do_first( environment: "Environment", seq: "t.Iterable[V]" ) -> "t.Union[V, Undefined]": """Return the first item of a sequence.""" @@ -674,6 +688,16 @@ def do_first( return environment.undefined("No first item, sequence was empty.") +@async_variant(sync_do_first) +async def do_first( + environment: "Environment", seq: "t.Union[t.AsyncIterable[V], t.Iterable[V]]" +) -> "t.Union[V, Undefined]": + try: + return t.cast("V", await auto_aiter(seq).__anext__()) + except StopAsyncIteration: + return environment.undefined("No first item, sequence was empty.") + + @pass_environment def do_last( environment: "Environment", seq: "t.Reversible[V]" @@ -693,6 +717,9 @@ def do_last( return environment.undefined("No last item, sequence was empty.") +# No async do_last, it may not be safe in async mode. + + @pass_context def do_random(context: "Context", seq: "t.Sequence[V]") -> "t.Union[V, Undefined]": """Return a random item from the sequence.""" @@ -1078,7 +1105,7 @@ def do_striptags(eval_ctx: "EvalContext", value: "t.Union[str, HasHTML]") -> str return eval_ctx.mark_safe(str(value)).striptags() -def do_slice( +def sync_do_slice( value: "t.Collection[V]", slices: int, fill_with: "t.Optional[V]" = None ) -> "t.Iterator[t.List[V]]": """Slice an iterator and return a list of lists containing @@ -1121,6 +1148,15 @@ def do_slice( yield tmp +@async_variant(sync_do_slice) +async def do_slice( + value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", + slices: int, + fill_with: t.Optional[t.Any] = None, +) -> "t.Iterator[t.List[V]]": + return sync_do_slice(await auto_to_list(value), slices, fill_with) + + def do_batch( value: "t.Iterable[V]", linecount: int, fill_with: "t.Optional[V]" = None ) -> "t.Iterator[t.List[V]]": @@ -1212,7 +1248,7 @@ def __str__(self): @pass_environment -def do_groupby( +def sync_do_groupby( environment: "Environment", value: "t.Iterable[V]", attribute: t.Union[str, int], @@ -1270,8 +1306,22 @@ def do_groupby( ] +@async_variant(sync_do_groupby) +async def do_groupby( + environment: "Environment", + value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", + attribute: t.Union[str, int], + default: t.Optional[t.Any] = None, +) -> "t.List[t.Tuple[t.Any, t.List[V]]]": + expr = make_attrgetter(environment, attribute, default=default) + return [ + _GroupTuple(key, await auto_to_list(values)) + for key, values in groupby(sorted(await auto_to_list(value), key=expr), expr) + ] + + @pass_environment -def do_sum( +def sync_do_sum( environment: "Environment", iterable: "t.Iterable[V]", attribute: t.Optional[t.Union[str, int]] = None, @@ -1297,13 +1347,40 @@ def do_sum( return sum(iterable, start) -def do_list(value: "t.Iterable[V]") -> "t.List[V]": +@async_variant(sync_do_sum) +async def do_sum( + environment: "Environment", + iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", + attribute: t.Optional[t.Union[str, int]] = None, + start: "V" = 0, # type: ignore +) -> "V": + rv = start + + if attribute is not None: + func = make_attrgetter(environment, attribute) + else: + + def func(x): + return x + + async for item in auto_aiter(iterable): + rv += func(item) + + return rv + + +def sync_do_list(value: "t.Iterable[V]") -> "t.List[V]": """Convert the value into a list. If it was a string the returned list will be a list of characters. """ return list(value) +@async_variant(sync_do_list) +async def do_list(value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]") -> "t.List[V]": + return await auto_to_list(value) + + @pass_eval_context def do_mark_safe(eval_ctx: "EvalContext", value: str) -> markupsafe.Markup: """Mark the value as safe which means that in an environment with automatic @@ -1377,14 +1454,14 @@ def do_attr( @typing.overload -def do_map( +def sync_do_map( context: "Context", value: t.Iterable, name: str, *args: t.Any, **kwargs: t.Any ) -> t.Iterable: ... @typing.overload -def do_map( +def sync_do_map( context: "Context", value: t.Iterable, *, @@ -1395,7 +1472,7 @@ def do_map( @pass_context -def do_map(context, value, *args, **kwargs): +def sync_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 only interested in a certain value of it. @@ -1442,8 +1519,39 @@ def do_map(context, value, *args, **kwargs): yield func(item) +@typing.overload +def do_map( + context: "Context", + value: t.Union[t.AsyncIterable, t.Iterable], + name: str, + *args: t.Any, + **kwargs: t.Any, +) -> t.Iterable: + ... + + +@typing.overload +def do_map( + context: "Context", + value: t.Union[t.AsyncIterable, t.Iterable], + *, + attribute: str = ..., + default: t.Optional[t.Any] = None, +) -> t.Iterable: + ... + + +@async_variant(sync_do_map) +async def do_map(context, value, *args, **kwargs): + if value: + func = prepare_map(context, args, kwargs) + + async for item in auto_aiter(value): + yield await auto_await(func(item)) + + @pass_context -def do_select( +def sync_do_select( context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any ) -> "t.Iterator[V]": """Filters a sequence of objects by applying a test to each object, @@ -1473,8 +1581,18 @@ def do_select( return select_or_reject(context, value, args, kwargs, lambda x: x, False) +@async_variant(sync_do_select) +async def do_select( + context: "Context", + value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", + *args: t.Any, + **kwargs: t.Any, +) -> "t.AsyncIterator[V]": + return async_select_or_reject(context, value, args, kwargs, lambda x: x, False) + + @pass_context -def do_reject( +def sync_do_reject( context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any ) -> "t.Iterator[V]": """Filters a sequence of objects by applying a test to each object, @@ -1499,8 +1617,18 @@ def do_reject( return select_or_reject(context, value, args, kwargs, lambda x: not x, False) +@async_variant(sync_do_reject) +async def do_reject( + context: "Context", + value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", + *args: t.Any, + **kwargs: t.Any, +) -> "t.AsyncIterator[V]": + return async_select_or_reject(context, value, args, kwargs, lambda x: not x, False) + + @pass_context -def do_selectattr( +def sync_do_selectattr( context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any ) -> "t.Iterator[V]": """Filters a sequence of objects by applying a test to the specified @@ -1529,8 +1657,18 @@ def do_selectattr( return select_or_reject(context, value, args, kwargs, lambda x: x, True) +@async_variant(sync_do_selectattr) +async def do_selectattr( + context: "Context", + value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", + *args: t.Any, + **kwargs: t.Any, +) -> "t.AsyncIterator[V]": + return async_select_or_reject(context, value, args, kwargs, lambda x: x, True) + + @pass_context -def do_rejectattr( +def sync_do_rejectattr( context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any ) -> "t.Iterator[V]": """Filters a sequence of objects by applying a test to the specified @@ -1557,6 +1695,16 @@ def do_rejectattr( return select_or_reject(context, value, args, kwargs, lambda x: not x, True) +@async_variant(sync_do_rejectattr) +async def do_rejectattr( + context: "Context", + value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", + *args: t.Any, + **kwargs: t.Any, +) -> "t.AsyncIterator[V]": + return async_select_or_reject(context, value, args, kwargs, lambda x: not x, True) + + @pass_eval_context def do_tojson( eval_ctx: "EvalContext", value: t.Any, indent: t.Optional[int] = None @@ -1664,6 +1812,22 @@ def select_or_reject( yield item +async def async_select_or_reject( + context: "Context", + value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", + args: t.Tuple, + kwargs: t.Dict[str, t.Any], + modfunc: t.Callable[[t.Any], t.Any], + lookup_attr: bool, +) -> "t.AsyncIterator[V]": + if value: + func = prepare_select_or_reject(context, args, kwargs, modfunc, lookup_attr) + + async for item in auto_aiter(value): + if func(item): + yield item + + FILTERS = { "abs": abs, "attr": do_attr, diff --git a/src/jinja2/nativetypes.py b/src/jinja2/nativetypes.py index 8867a3165..6cca518c3 100644 --- a/src/jinja2/nativetypes.py +++ b/src/jinja2/nativetypes.py @@ -86,10 +86,10 @@ def render(self, *args, **kwargs): with :func:`ast.literal_eval`, the parsed value is returned. Otherwise, the string is returned. """ - vars = dict(*args, **kwargs) + ctx = self.new_context(dict(*args, **kwargs)) try: - return native_concat(self.root_render_func(self.new_context(vars))) + return native_concat(self.root_render_func(ctx)) except Exception: return self.environment.handle_exception() @@ -99,8 +99,7 @@ async def render_async(self, *args, **kwargs): "The environment was not created with async mode enabled." ) - vars = dict(*args, **kwargs) - ctx = self.new_context(vars) + ctx = self.new_context(dict(*args, **kwargs)) try: return native_concat([n async for n in self.root_render_func(ctx)]) diff --git a/src/jinja2/runtime.py b/src/jinja2/runtime.py index a386894d1..7ca9f0831 100644 --- a/src/jinja2/runtime.py +++ b/src/jinja2/runtime.py @@ -3,11 +3,12 @@ import typing as t from collections import abc from itertools import chain -from types import MethodType from markupsafe import escape as html_escape # noqa: F401 from markupsafe import soft_str +from .async_utils import auto_aiter +from .async_utils import auto_await # noqa: F401 from .exceptions import TemplateNotFound # noqa: F401 from .exceptions import TemplateRuntimeError # noqa: F401 from .exceptions import UndefinedError @@ -40,6 +41,11 @@ "Undefined", "internalcode", ] +async_exported = [ + "AsyncLoopContext", + "auto_aiter", + "auto_await", +] def identity(x): @@ -132,45 +138,34 @@ def __repr__(self): return f"<{self.__class__.__name__} {self.__context.name!r}>" -def _get_func(x): - return getattr(x, "__func__", x) - - class ContextMeta(type): def __new__(mcs, name, bases, d): rv = type.__new__(mcs, name, bases, d) - if bases == (): - return rv - resolve = _get_func(rv.resolve) - default_resolve = _get_func(Context.resolve) - resolve_or_missing = _get_func(rv.resolve_or_missing) - default_resolve_or_missing = _get_func(Context.resolve_or_missing) + if not bases: + return rv - # If we have a changed resolve but no changed default or missing - # resolve we invert the call logic. - if ( - resolve is not default_resolve - and resolve_or_missing is default_resolve_or_missing - ): + if "resolve_or_missing" in d: + # If the subclass overrides resolve_or_missing it opts in to + # modern mode no matter what. + rv._legacy_resolve_mode = False + elif "resolve" in d or rv._legacy_resolve_mode: + # If the subclass overrides resolve, or if its base is + # already in legacy mode, warn about legacy behavior. + import warnings + + warnings.warn( + "Overriding 'resolve' is deprecated and will not have" + " the expected behavior in Jinja 3.1. Override" + " 'resolve_or_missing' instead ", + DeprecationWarning, + stacklevel=2, + ) rv._legacy_resolve_mode = True - elif ( - resolve is default_resolve - and resolve_or_missing is default_resolve_or_missing - ): - rv._fast_resolve_mode = True return rv -def resolve_or_missing(context, key, missing=missing): - if key in context.vars: - return context.vars[key] - if key in context.parent: - return context.parent[key] - return missing - - @abc.Mapping.register class Context(metaclass=ContextMeta): """The template context holds the variables of a template. It stores the @@ -192,10 +187,7 @@ class Context(metaclass=ContextMeta): :class:`Undefined` object for missing variables. """ - # XXX: we want to eventually make this be a deprecation warning and - # remove it. _legacy_resolve_mode = False - _fast_resolve_mode = False def __init__(self, environment, parent, name, blocks, globals=None): self.parent = parent @@ -211,11 +203,6 @@ def __init__(self, environment, parent, name, blocks, globals=None): # from the template. self.blocks = {k: [v] for k, v in blocks.items()} - # In case we detect the fast resolve mode we can set up an alias - # here that bypasses the legacy code logic. - if self._fast_resolve_mode: - self.resolve_or_missing = MethodType(resolve_or_missing, self) - def super(self, name, current): """Render a parent block.""" try: @@ -229,8 +216,11 @@ def super(self, name, current): return BlockReference(name, self, blocks, index) def get(self, key, default=None): - """Returns an item from the template context, if it doesn't exist - `default` is returned. + """Look up a variable by name, or return a default if the key is + not found. + + :param key: The variable name to look up. + :param default: The value to return if the key is not found. """ try: return self[key] @@ -238,27 +228,56 @@ def get(self, key, default=None): return default def resolve(self, key): - """Looks up a variable like `__getitem__` or `get` but returns an - :class:`Undefined` object with the name of the name looked up. + """Look up a variable by name, or return an :class:`Undefined` + object if the key is not found. + + If you need to add custom behavior, override + :meth:`resolve_or_missing`, not this method. The various lookup + functions use that method, not this one. + + :param key: The variable name to look up. """ if self._legacy_resolve_mode: - rv = resolve_or_missing(self, key) - else: - rv = self.resolve_or_missing(key) + if key in self.vars: + return self.vars[key] + + if key in self.parent: + return self.parent[key] + + return self.environment.undefined(name=key) + + rv = self.resolve_or_missing(key) + if rv is missing: return self.environment.undefined(name=key) + return rv def resolve_or_missing(self, key): - """Resolves a variable like :meth:`resolve` but returns the - special `missing` value if it cannot be found. + """Look up a variable by name, or return a ``missing`` sentinel + if the key is not found. + + Override this method to add custom lookup behavior. + :meth:`resolve`, :meth:`get`, and :meth:`__getitem__` use this + method. Don't call this method directly. + + :param key: The variable name to look up. """ if self._legacy_resolve_mode: rv = self.resolve(key) + if isinstance(rv, Undefined): - rv = missing + return missing + return rv - return resolve_or_missing(self, key) + + if key in self.vars: + return self.vars[key] + + if key in self.parent: + return self.parent[key] + + return missing def get_exported(self): """Get a new dict with the exported variables.""" @@ -348,12 +367,14 @@ def __contains__(self, name): return name in self.vars or name in self.parent def __getitem__(self, key): - """Lookup a variable or raise `KeyError` if the variable is - undefined. + """Look up a variable by name with ``[]`` syntax, or raise a + ``KeyError`` if the key is not found. """ item = self.resolve_or_missing(key) + if item is missing: raise KeyError(key) + return item def __repr__(self): @@ -378,9 +399,22 @@ def super(self): ) return BlockReference(self.name, self._context, self._stack, self._depth + 1) + @internalcode + async def _async_call(self): + rv = concat([x async for x in self._stack[self._depth](self._context)]) + + if self._context.eval_ctx.autoescape: + return self._context.eval_ctx.mark_safe(rv) + + return rv + @internalcode def __call__(self): + if self._context.environment.is_async: + return self._async_call() + rv = concat(self._stack[self._depth](self._context)) + if self._context.eval_ctx.autoescape: rv = self._context.eval_ctx.mark_safe(rv) return rv @@ -577,6 +611,73 @@ def __repr__(self): return f"<{self.__class__.__name__} {self.index}/{self.length}>" +class AsyncLoopContext(LoopContext): + @staticmethod + def _to_iterator(iterable): + return auto_aiter(iterable) + + @property + async def length(self): + if self._length is not None: + return self._length + + try: + self._length = len(self._iterable) + except TypeError: + iterable = [x async for x in self._iterator] + self._iterator = self._to_iterator(iterable) + self._length = len(iterable) + self.index + (self._after is not missing) + + return self._length + + @property + async def revindex0(self): + return await self.length - self.index + + @property + async def revindex(self): + return await self.length - self.index0 + + async def _peek_next(self): + if self._after is not missing: + return self._after + + try: + self._after = await self._iterator.__anext__() + except StopAsyncIteration: + self._after = missing + + return self._after + + @property + async def last(self): + return await self._peek_next() is missing + + @property + async def nextitem(self): + rv = await self._peek_next() + + if rv is missing: + return self._undefined("there is no next item") + + return rv + + def __aiter__(self): + return self + + async def __anext__(self): + if self._after is not missing: + rv = self._after + self._after = missing + else: + rv = await self._iterator.__anext__() + + self.index0 += 1 + self._before = self._current + self._current = rv + return rv, self + + class Macro: """Wraps a macro function.""" @@ -689,9 +790,20 @@ def __call__(self, *args, **kwargs): return self._invoke(arguments, autoescape) + async def _async_invoke(self, arguments, autoescape): + rv = await self._func(*arguments) + + if autoescape: + return self._mark_safe(rv) + + return rv + def _invoke(self, arguments, autoescape): - """This method is being swapped out by the async implementation.""" + if self._environment.is_async: + return self._async_invoke(arguments, autoescape) + rv = self._func(*arguments) + if autoescape: rv = self._mark_safe(rv) return rv diff --git a/src/jinja2/utils.py b/src/jinja2/utils.py index 704a0b651..a42064273 100644 --- a/src/jinja2/utils.py +++ b/src/jinja2/utils.py @@ -24,13 +24,10 @@ # special singleton representing missing values for the runtime missing = type("MissingType", (), {"__repr__": lambda x: "missing"})() -# internal code internal_code: t.MutableSet[CodeType] = set() concat = "".join -_slash_escape = "\\/" not in json.dumps("/") - def pass_context(f: "F") -> "F": """Pass the :class:`~jinja2.runtime.Context` as the first argument @@ -944,14 +941,6 @@ def __repr__(self): return f"" -# does this python version support async for in and async generators? -try: - exec("async def _():\n async for _ in ():\n yield _") - have_async_gen = True -except SyntaxError: - have_async_gen = False - - class Markup(markupsafe.Markup): def __init__(self, *args, **kwargs): warnings.warn( diff --git a/tests/conftest.py b/tests/conftest.py index 71d110cae..ec2bbcfd2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,15 +2,8 @@ import pytest -from jinja2 import Environment from jinja2 import loaders -from jinja2.utils import have_async_gen - - -def pytest_ignore_collect(path): - if "async" in path.basename and not have_async_gen: - return True - return False +from jinja2.environment import Environment @pytest.fixture diff --git a/tests/test_async.py b/tests/test_async.py index 50c1f18ca..833b4d9b3 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -6,7 +6,7 @@ from jinja2 import DictLoader from jinja2 import Environment from jinja2 import Template -from jinja2.asyncsupport import auto_aiter +from jinja2.async_utils import auto_aiter from jinja2.exceptions import TemplateNotFound from jinja2.exceptions import TemplatesNotFound from jinja2.exceptions import UndefinedError diff --git a/tests/test_asyncfilters.py b/tests/test_async_filters.py similarity index 99% rename from tests/test_asyncfilters.py rename to tests/test_async_filters.py index d76ae4a83..7a16ca309 100644 --- a/tests/test_asyncfilters.py +++ b/tests/test_async_filters.py @@ -4,7 +4,7 @@ from markupsafe import Markup from jinja2 import Environment -from jinja2.asyncsupport import auto_aiter +from jinja2.async_utils import auto_aiter async def make_aiter(iter): diff --git a/tests/test_regression.py b/tests/test_regression.py index 5d7a7fb23..4491dab2d 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -594,11 +594,13 @@ class MyEnvironment(Environment): def test_legacy_custom_context(self, env): from jinja2.runtime import Context, missing - class MyContext(Context): - def resolve(self, name): - if name == "foo": - return 42 - return super().resolve(name) + with pytest.deprecated_call(): + + class MyContext(Context): + def resolve(self, name): + if name == "foo": + return 42 + return super().resolve(name) x = MyContext(env, parent={"bar": 23}, name="foo", blocks={}) assert x._legacy_resolve_mode