templating.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import typing
  2. import warnings
  3. from os import PathLike
  4. from starlette.background import BackgroundTask
  5. from starlette.datastructures import URL
  6. from starlette.requests import Request
  7. from starlette.responses import HTMLResponse
  8. from starlette.types import Receive, Scope, Send
  9. try:
  10. import jinja2
  11. # @contextfunction was renamed to @pass_context in Jinja 3.0, and was removed in 3.1
  12. # hence we try to get pass_context (most installs will be >=3.1)
  13. # and fall back to contextfunction,
  14. # adding a type ignore for mypy to let us access an attribute that may not exist
  15. if hasattr(jinja2, "pass_context"):
  16. pass_context = jinja2.pass_context
  17. else: # pragma: nocover
  18. pass_context = jinja2.contextfunction # type: ignore[attr-defined]
  19. except ModuleNotFoundError: # pragma: nocover
  20. jinja2 = None # type: ignore[assignment]
  21. class _TemplateResponse(HTMLResponse):
  22. def __init__(
  23. self,
  24. template: typing.Any,
  25. context: typing.Dict[str, typing.Any],
  26. status_code: int = 200,
  27. headers: typing.Optional[typing.Mapping[str, str]] = None,
  28. media_type: typing.Optional[str] = None,
  29. background: typing.Optional[BackgroundTask] = None,
  30. ):
  31. self.template = template
  32. self.context = context
  33. content = template.render(context)
  34. super().__init__(content, status_code, headers, media_type, background)
  35. async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
  36. request = self.context.get("request", {})
  37. extensions = request.get("extensions", {})
  38. if "http.response.debug" in extensions:
  39. await send(
  40. {
  41. "type": "http.response.debug",
  42. "info": {
  43. "template": self.template,
  44. "context": self.context,
  45. },
  46. }
  47. )
  48. await super().__call__(scope, receive, send)
  49. class Jinja2Templates:
  50. """
  51. templates = Jinja2Templates("templates")
  52. return templates.TemplateResponse("index.html", {"request": request})
  53. """
  54. @typing.overload
  55. def __init__(
  56. self,
  57. directory: "typing.Union[str, PathLike[typing.AnyStr], typing.Sequence[typing.Union[str, PathLike[typing.AnyStr]]]]", # noqa: E501
  58. *,
  59. context_processors: typing.Optional[
  60. typing.List[typing.Callable[[Request], typing.Dict[str, typing.Any]]]
  61. ] = None,
  62. **env_options: typing.Any,
  63. ) -> None:
  64. ...
  65. @typing.overload
  66. def __init__(
  67. self,
  68. *,
  69. env: "jinja2.Environment",
  70. context_processors: typing.Optional[
  71. typing.List[typing.Callable[[Request], typing.Dict[str, typing.Any]]]
  72. ] = None,
  73. ) -> None:
  74. ...
  75. def __init__(
  76. self,
  77. directory: "typing.Union[str, PathLike[typing.AnyStr], typing.Sequence[typing.Union[str, PathLike[typing.AnyStr]]], None]" = None, # noqa: E501
  78. *,
  79. context_processors: typing.Optional[
  80. typing.List[typing.Callable[[Request], typing.Dict[str, typing.Any]]]
  81. ] = None,
  82. env: typing.Optional["jinja2.Environment"] = None,
  83. **env_options: typing.Any,
  84. ) -> None:
  85. if env_options:
  86. warnings.warn(
  87. "Extra environment options are deprecated. Use a preconfigured jinja2.Environment instead.", # noqa: E501
  88. DeprecationWarning,
  89. )
  90. assert jinja2 is not None, "jinja2 must be installed to use Jinja2Templates"
  91. assert directory or env, "either 'directory' or 'env' arguments must be passed"
  92. self.context_processors = context_processors or []
  93. if directory is not None:
  94. self.env = self._create_env(directory, **env_options)
  95. elif env is not None:
  96. self.env = env
  97. self._setup_env_defaults(self.env)
  98. def _create_env(
  99. self,
  100. directory: "typing.Union[str, PathLike[typing.AnyStr], typing.Sequence[typing.Union[str, PathLike[typing.AnyStr]]]]", # noqa: E501
  101. **env_options: typing.Any,
  102. ) -> "jinja2.Environment":
  103. loader = jinja2.FileSystemLoader(directory)
  104. env_options.setdefault("loader", loader)
  105. env_options.setdefault("autoescape", True)
  106. return jinja2.Environment(**env_options)
  107. def _setup_env_defaults(self, env: "jinja2.Environment") -> None:
  108. @pass_context
  109. def url_for(
  110. context: typing.Dict[str, typing.Any],
  111. name: str,
  112. /,
  113. **path_params: typing.Any,
  114. ) -> URL:
  115. request: Request = context["request"]
  116. return request.url_for(name, **path_params)
  117. env.globals.setdefault("url_for", url_for)
  118. def get_template(self, name: str) -> "jinja2.Template":
  119. return self.env.get_template(name)
  120. @typing.overload
  121. def TemplateResponse(
  122. self,
  123. request: Request,
  124. name: str,
  125. context: typing.Optional[typing.Dict[str, typing.Any]] = None,
  126. status_code: int = 200,
  127. headers: typing.Optional[typing.Mapping[str, str]] = None,
  128. media_type: typing.Optional[str] = None,
  129. background: typing.Optional[BackgroundTask] = None,
  130. ) -> _TemplateResponse:
  131. ...
  132. @typing.overload
  133. def TemplateResponse(
  134. self,
  135. name: str,
  136. context: typing.Optional[typing.Dict[str, typing.Any]] = None,
  137. status_code: int = 200,
  138. headers: typing.Optional[typing.Mapping[str, str]] = None,
  139. media_type: typing.Optional[str] = None,
  140. background: typing.Optional[BackgroundTask] = None,
  141. ) -> _TemplateResponse:
  142. # Deprecated usage
  143. ...
  144. def TemplateResponse(
  145. self, *args: typing.Any, **kwargs: typing.Any
  146. ) -> _TemplateResponse:
  147. if args:
  148. if isinstance(
  149. args[0], str
  150. ): # the first argument is template name (old style)
  151. warnings.warn(
  152. "The `name` is not the first parameter anymore. "
  153. "The first parameter should be the `Request` instance.\n"
  154. 'Replace `TemplateResponse(name, {"request": request})` by `TemplateResponse(request, name)`.', # noqa: E501
  155. DeprecationWarning,
  156. )
  157. name = args[0]
  158. context = args[1] if len(args) > 1 else kwargs.get("context", {})
  159. status_code = (
  160. args[2] if len(args) > 2 else kwargs.get("status_code", 200)
  161. )
  162. headers = args[2] if len(args) > 2 else kwargs.get("headers")
  163. media_type = args[3] if len(args) > 3 else kwargs.get("media_type")
  164. background = args[4] if len(args) > 4 else kwargs.get("background")
  165. if "request" not in context:
  166. raise ValueError('context must include a "request" key')
  167. request = context["request"]
  168. else: # the first argument is a request instance (new style)
  169. request = args[0]
  170. name = args[1] if len(args) > 1 else kwargs["name"]
  171. context = args[2] if len(args) > 2 else kwargs.get("context", {})
  172. status_code = (
  173. args[3] if len(args) > 3 else kwargs.get("status_code", 200)
  174. )
  175. headers = args[4] if len(args) > 4 else kwargs.get("headers")
  176. media_type = args[5] if len(args) > 5 else kwargs.get("media_type")
  177. background = args[6] if len(args) > 6 else kwargs.get("background")
  178. else: # all arguments are kwargs
  179. if "request" not in kwargs:
  180. warnings.warn(
  181. "The `TemplateResponse` now requires the `request` argument.\n"
  182. 'Replace `TemplateResponse(name, {"context": context})` by `TemplateResponse(request, name)`.', # noqa: E501
  183. DeprecationWarning,
  184. )
  185. if "request" not in kwargs.get("context", {}):
  186. raise ValueError('context must include a "request" key')
  187. context = kwargs.get("context", {})
  188. request = kwargs.get("request", context.get("request"))
  189. name = typing.cast(str, kwargs["name"])
  190. status_code = kwargs.get("status_code", 200)
  191. headers = kwargs.get("headers")
  192. media_type = kwargs.get("media_type")
  193. background = kwargs.get("background")
  194. context.setdefault("request", request)
  195. for context_processor in self.context_processors:
  196. context.update(context_processor(request))
  197. template = self.get_template(name)
  198. return _TemplateResponse(
  199. template,
  200. context,
  201. status_code=status_code,
  202. headers=headers,
  203. media_type=media_type,
  204. background=background,
  205. )