templating.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. from __future__ import annotations
  2. from collections.abc import Callable, Mapping, Sequence
  3. from os import PathLike
  4. from typing import TYPE_CHECKING, Any, overload
  5. from starlette.background import BackgroundTask
  6. from starlette.datastructures import URL
  7. from starlette.requests import Request
  8. from starlette.responses import HTMLResponse
  9. from starlette.types import Receive, Scope, Send
  10. try:
  11. import jinja2
  12. # @contextfunction was renamed to @pass_context in Jinja 3.0, and was removed in 3.1
  13. # hence we try to get pass_context (most installs will be >=3.1)
  14. # and fall back to contextfunction,
  15. # adding a type ignore for mypy to let us access an attribute that may not exist
  16. if TYPE_CHECKING:
  17. pass_context = jinja2.pass_context
  18. else:
  19. if hasattr(jinja2, "pass_context"):
  20. pass_context = jinja2.pass_context
  21. else: # pragma: no cover
  22. pass_context = jinja2.contextfunction # type: ignore[attr-defined]
  23. except ImportError as _import_error: # pragma: no cover
  24. raise ImportError("jinja2 must be installed to use Jinja2Templates") from _import_error
  25. class _TemplateResponse(HTMLResponse):
  26. def __init__(
  27. self,
  28. template: Any,
  29. context: dict[str, Any],
  30. status_code: int = 200,
  31. headers: Mapping[str, str] | None = None,
  32. media_type: str | None = None,
  33. background: BackgroundTask | None = None,
  34. ):
  35. self.template = template
  36. self.context = context
  37. content = template.render(context)
  38. super().__init__(content, status_code, headers, media_type, background)
  39. async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
  40. request = self.context.get("request", {})
  41. extensions = request.get("extensions", {})
  42. if "http.response.debug" in extensions: # pragma: no branch
  43. await send({"type": "http.response.debug", "info": {"template": self.template, "context": self.context}})
  44. await super().__call__(scope, receive, send)
  45. class Jinja2Templates:
  46. """Jinja2 template renderer.
  47. Example:
  48. ```python
  49. from starlette.templating import Jinja2Templates
  50. templates = Jinja2Templates(directory="templates")
  51. async def homepage(request: Request) -> Response:
  52. return templates.TemplateResponse(request, "index.html")
  53. ```
  54. """
  55. @overload
  56. def __init__(
  57. self,
  58. directory: str | PathLike[str] | Sequence[str | PathLike[str]],
  59. *,
  60. context_processors: list[Callable[[Request], dict[str, Any]]] | None = None,
  61. ) -> None: ...
  62. @overload
  63. def __init__(
  64. self,
  65. *,
  66. env: jinja2.Environment,
  67. context_processors: list[Callable[[Request], dict[str, Any]]] | None = None,
  68. ) -> None: ...
  69. def __init__(
  70. self,
  71. directory: str | PathLike[str] | Sequence[str | PathLike[str]] | None = None,
  72. *,
  73. context_processors: list[Callable[[Request], dict[str, Any]]] | None = None,
  74. env: jinja2.Environment | None = None,
  75. ) -> None:
  76. assert bool(directory) ^ bool(env), "either 'directory' or 'env' arguments must be passed"
  77. self.context_processors = context_processors or []
  78. if directory is not None:
  79. loader = jinja2.FileSystemLoader(directory)
  80. self.env = jinja2.Environment(loader=loader, autoescape=jinja2.select_autoescape())
  81. elif env is not None: # pragma: no branch
  82. self.env = env
  83. self._setup_env_defaults(self.env)
  84. def _setup_env_defaults(self, env: jinja2.Environment) -> None:
  85. @pass_context
  86. def url_for(
  87. context: dict[str, Any],
  88. name: str,
  89. /,
  90. **path_params: Any,
  91. ) -> URL:
  92. request: Request = context["request"]
  93. return request.url_for(name, **path_params)
  94. env.globals.setdefault("url_for", url_for)
  95. def get_template(self, name: str) -> jinja2.Template:
  96. return self.env.get_template(name)
  97. def TemplateResponse(
  98. self,
  99. request: Request,
  100. name: str,
  101. context: dict[str, Any] | None = None,
  102. status_code: int = 200,
  103. headers: Mapping[str, str] | None = None,
  104. media_type: str | None = None,
  105. background: BackgroundTask | None = None,
  106. ) -> _TemplateResponse:
  107. """
  108. Render a template and return an HTML response.
  109. Args:
  110. request: The incoming request instance.
  111. name: The template file name to render.
  112. context: Variables to pass to the template.
  113. status_code: HTTP status code for the response.
  114. headers: Additional headers to include in the response.
  115. media_type: Media type for the response.
  116. background: Background task to run after response is sent.
  117. Returns:
  118. An HTML response with the rendered template content.
  119. """
  120. context = context or {}
  121. context.setdefault("request", request)
  122. for context_processor in self.context_processors:
  123. context.update(context_processor(request))
  124. template = self.get_template(name)
  125. return _TemplateResponse(
  126. template,
  127. context,
  128. status_code=status_code,
  129. headers=headers,
  130. media_type=media_type,
  131. background=background,
  132. )