import functools
from typing import (
Any,
Awaitable,
Callable,
Dict,
Final,
Mapping,
Optional,
Protocol,
Sequence,
Tuple,
TypeVar,
Union,
overload,
)
import jinja2
from aiohttp import web
from aiohttp.abc import AbstractView
from .helpers import GLOBAL_HELPERS, static_root_key
from .typedefs import Filters
__version__ = "1.6"
__all__ = (
"get_env",
"render_string",
"render_template",
"setup",
"static_root_key",
"template",
)
_TemplateReturnType = Awaitable[Union[web.StreamResponse, Mapping[str, Any]]]
_SimpleTemplateHandler = Callable[[web.Request], _TemplateReturnType]
_ContextProcessor = Callable[[web.Request], Awaitable[Dict[str, Any]]]
APP_CONTEXT_PROCESSORS_KEY: Final = web.AppKey[Sequence[_ContextProcessor]](
"APP_CONTEXT_PROCESSORS_KEY"
)
APP_KEY: Final = web.AppKey[jinja2.Environment]("APP_KEY")
REQUEST_CONTEXT_KEY: Final = "aiohttp_jinja2_context"
_T = TypeVar("_T")
_AbstractView = TypeVar("_AbstractView", bound=AbstractView)
class _TemplateWrapper(Protocol):
@overload
def __call__(
self, func: _SimpleTemplateHandler
) -> Callable[[web.Request], Awaitable[web.StreamResponse]]:
...
@overload
def __call__(
self, func: Callable[[_AbstractView], _TemplateReturnType]
) -> Callable[[_AbstractView], Awaitable[web.StreamResponse]]:
...
@overload
def __call__(
self, func: Callable[[_T, web.Request], _TemplateReturnType]
) -> Callable[[_T, web.Request], Awaitable[web.StreamResponse]]:
...
[docs]
def setup(
app: web.Application,
*args: Any,
app_key: web.AppKey[jinja2.Environment] = APP_KEY,
context_processors: Sequence[_ContextProcessor] = (),
filters: Optional[Filters] = None,
default_helpers: bool = True,
**kwargs: Any,
) -> jinja2.Environment:
kwargs.setdefault("autoescape", True)
env = jinja2.Environment(*args, **kwargs)
if default_helpers:
env.globals.update(GLOBAL_HELPERS)
if filters is not None:
env.filters.update(filters)
app[app_key] = env
if context_processors:
app[APP_CONTEXT_PROCESSORS_KEY] = context_processors
app.middlewares.append(context_processors_middleware)
env.globals["app"] = app
return env
[docs]
def get_env(
app: web.Application, *, app_key: web.AppKey[jinja2.Environment] = APP_KEY
) -> jinja2.Environment:
try:
return app[app_key]
except KeyError:
raise RuntimeError("aiohttp_jinja2.setup(...) must be called first.")
def _render_string(
template_name: str,
request: web.Request,
context: Mapping[str, Any],
app_key: web.AppKey[jinja2.Environment],
) -> Tuple[jinja2.Template, Mapping[str, Any]]:
env = request.config_dict.get(app_key)
if env is None:
text = "Template engine is not initialized, call aiohttp_jinja2.setup() first"
# in order to see meaningful exception message both: on console
# output and rendered page we add same message to *reason* and
# *text* arguments.
raise web.HTTPInternalServerError(reason=text, text=text)
try:
template = env.get_template(template_name)
except jinja2.TemplateNotFound as e:
text = f"Template '{template_name}' not found"
raise web.HTTPInternalServerError(reason=text, text=text) from e
if not isinstance(context, Mapping):
text = f"context should be mapping, not {type(context)}" # type: ignore[unreachable]
# same reason as above
raise web.HTTPInternalServerError(reason=text, text=text)
if request.get(REQUEST_CONTEXT_KEY):
context = dict(request[REQUEST_CONTEXT_KEY], **context)
return template, context
[docs]
def render_string(
template_name: str,
request: web.Request,
context: Mapping[str, Any],
*,
app_key: web.AppKey[jinja2.Environment] = APP_KEY,
) -> str:
template, context = _render_string(template_name, request, context, app_key)
return template.render(context)
[docs]
async def render_string_async(
template_name: str,
request: web.Request,
context: Mapping[str, Any],
*,
app_key: web.AppKey[jinja2.Environment] = APP_KEY,
) -> str:
template, context = _render_string(template_name, request, context, app_key)
return await template.render_async(context)
def _render_template(
context: Optional[Mapping[str, Any]],
encoding: str,
status: int,
) -> Tuple[web.Response, Mapping[str, Any]]:
response = web.Response(status=status)
if context is None:
context = {}
response.content_type = "text/html"
response.charset = encoding
return response, context
[docs]
def render_template(
template_name: str,
request: web.Request,
context: Optional[Mapping[str, Any]],
*,
app_key: web.AppKey[jinja2.Environment] = APP_KEY,
encoding: str = "utf-8",
status: int = 200,
) -> web.Response:
response, context = _render_template(context, encoding, status)
response.text = render_string(template_name, request, context, app_key=app_key)
return response
[docs]
async def render_template_async(
template_name: str,
request: web.Request,
context: Optional[Mapping[str, Any]],
*,
app_key: web.AppKey[jinja2.Environment] = APP_KEY,
encoding: str = "utf-8",
status: int = 200,
) -> web.Response:
response, context = _render_template(context, encoding, status)
response.text = await render_string_async(
template_name, request, context, app_key=app_key
)
return response
[docs]
def template(
template_name: str,
*,
app_key: web.AppKey[jinja2.Environment] = APP_KEY,
encoding: str = "utf-8",
status: int = 200,
) -> _TemplateWrapper:
@overload
def wrapper(
func: _SimpleTemplateHandler,
) -> Callable[[web.Request], Awaitable[web.StreamResponse]]:
...
@overload
def wrapper(
func: Callable[[_AbstractView], _TemplateReturnType]
) -> Callable[[_AbstractView], Awaitable[web.StreamResponse]]:
...
@overload
def wrapper(
func: Callable[[_T, web.Request], _TemplateReturnType]
) -> Callable[[_T, web.Request], Awaitable[web.StreamResponse]]:
...
def wrapper(
func: Callable[..., _TemplateReturnType]
) -> Callable[..., Awaitable[web.StreamResponse]]:
# TODO(PY310): ParamSpec
@functools.wraps(func)
async def wrapped(*args: Any) -> web.StreamResponse: # type: ignore[misc]
context = await func(*args)
if isinstance(context, web.StreamResponse):
return context
# Supports class based views see web.View
if isinstance(args[0], AbstractView):
request = args[0].request
else:
request = args[-1]
env = request.config_dict.get(app_key)
if env and env.is_async:
response = await render_template_async(
template_name, request, context, app_key=app_key, encoding=encoding
)
else:
response = render_template(
template_name, request, context, app_key=app_key, encoding=encoding
)
response.set_status(status)
return response
return wrapped
return wrapper
@web.middleware
async def context_processors_middleware(
request: web.Request,
handler: Callable[[web.Request], Awaitable[web.StreamResponse]],
) -> web.StreamResponse:
if REQUEST_CONTEXT_KEY not in request:
request[REQUEST_CONTEXT_KEY] = {}
for processor in request.config_dict[APP_CONTEXT_PROCESSORS_KEY]:
request[REQUEST_CONTEXT_KEY].update(await processor(request))
return await handler(request)
async def request_processor(request: web.Request) -> Dict[str, web.Request]:
return {"request": request}