applications.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. from __future__ import annotations
  2. import sys
  3. import typing
  4. import warnings
  5. if sys.version_info >= (3, 10): # pragma: no cover
  6. from typing import ParamSpec
  7. else: # pragma: no cover
  8. from typing_extensions import ParamSpec
  9. from starlette.datastructures import State, URLPath
  10. from starlette.middleware import Middleware, _MiddlewareClass
  11. from starlette.middleware.base import BaseHTTPMiddleware
  12. from starlette.middleware.errors import ServerErrorMiddleware
  13. from starlette.middleware.exceptions import ExceptionMiddleware
  14. from starlette.requests import Request
  15. from starlette.responses import Response
  16. from starlette.routing import BaseRoute, Router
  17. from starlette.types import ASGIApp, ExceptionHandler, Lifespan, Receive, Scope, Send
  18. from starlette.websockets import WebSocket
  19. AppType = typing.TypeVar("AppType", bound="Starlette")
  20. P = ParamSpec("P")
  21. class Starlette:
  22. """
  23. Creates an application instance.
  24. **Parameters:**
  25. * **debug** - Boolean indicating if debug tracebacks should be returned on errors.
  26. * **routes** - A list of routes to serve incoming HTTP and WebSocket requests.
  27. * **middleware** - A list of middleware to run for every request. A starlette
  28. application will always automatically include two middleware classes.
  29. `ServerErrorMiddleware` is added as the very outermost middleware, to handle
  30. any uncaught errors occurring anywhere in the entire stack.
  31. `ExceptionMiddleware` is added as the very innermost middleware, to deal
  32. with handled exception cases occurring in the routing or endpoints.
  33. * **exception_handlers** - A mapping of either integer status codes,
  34. or exception class types onto callables which handle the exceptions.
  35. Exception handler callables should be of the form
  36. `handler(request, exc) -> response` and may be either standard functions, or
  37. async functions.
  38. * **on_startup** - A list of callables to run on application startup.
  39. Startup handler callables do not take any arguments, and may be either
  40. standard functions, or async functions.
  41. * **on_shutdown** - A list of callables to run on application shutdown.
  42. Shutdown handler callables do not take any arguments, and may be either
  43. standard functions, or async functions.
  44. * **lifespan** - A lifespan context function, which can be used to perform
  45. startup and shutdown tasks. This is a newer style that replaces the
  46. `on_startup` and `on_shutdown` handlers. Use one or the other, not both.
  47. """
  48. def __init__(
  49. self: "AppType",
  50. debug: bool = False,
  51. routes: typing.Sequence[BaseRoute] | None = None,
  52. middleware: typing.Sequence[Middleware] | None = None,
  53. exception_handlers: typing.Mapping[typing.Any, ExceptionHandler] | None = None,
  54. on_startup: typing.Sequence[typing.Callable[[], typing.Any]] | None = None,
  55. on_shutdown: typing.Sequence[typing.Callable[[], typing.Any]] | None = None,
  56. lifespan: typing.Optional[Lifespan["AppType"]] = None,
  57. ) -> None:
  58. # The lifespan context function is a newer style that replaces
  59. # on_startup / on_shutdown handlers. Use one or the other, not both.
  60. assert lifespan is None or (
  61. on_startup is None and on_shutdown is None
  62. ), "Use either 'lifespan' or 'on_startup'/'on_shutdown', not both."
  63. self.debug = debug
  64. self.state = State()
  65. self.router = Router(
  66. routes, on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan
  67. )
  68. self.exception_handlers = (
  69. {} if exception_handlers is None else dict(exception_handlers)
  70. )
  71. self.user_middleware = [] if middleware is None else list(middleware)
  72. self.middleware_stack: typing.Optional[ASGIApp] = None
  73. def build_middleware_stack(self) -> ASGIApp:
  74. debug = self.debug
  75. error_handler = None
  76. exception_handlers: typing.Dict[
  77. typing.Any, typing.Callable[[Request, Exception], Response]
  78. ] = {}
  79. for key, value in self.exception_handlers.items():
  80. if key in (500, Exception):
  81. error_handler = value
  82. else:
  83. exception_handlers[key] = value
  84. middleware = (
  85. [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
  86. + self.user_middleware
  87. + [
  88. Middleware(
  89. ExceptionMiddleware, handlers=exception_handlers, debug=debug
  90. )
  91. ]
  92. )
  93. app = self.router
  94. for cls, args, kwargs in reversed(middleware):
  95. app = cls(app=app, *args, **kwargs)
  96. return app
  97. @property
  98. def routes(self) -> typing.List[BaseRoute]:
  99. return self.router.routes
  100. def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
  101. return self.router.url_path_for(name, **path_params)
  102. async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
  103. scope["app"] = self
  104. if self.middleware_stack is None:
  105. self.middleware_stack = self.build_middleware_stack()
  106. await self.middleware_stack(scope, receive, send)
  107. def on_event(self, event_type: str) -> typing.Callable: # type: ignore[type-arg]
  108. return self.router.on_event(event_type) # pragma: nocover
  109. def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None:
  110. self.router.mount(path, app=app, name=name) # pragma: no cover
  111. def host(self, host: str, app: ASGIApp, name: str | None = None) -> None:
  112. self.router.host(host, app=app, name=name) # pragma: no cover
  113. def add_middleware(
  114. self,
  115. middleware_class: typing.Type[_MiddlewareClass[P]],
  116. *args: P.args,
  117. **kwargs: P.kwargs,
  118. ) -> None:
  119. if self.middleware_stack is not None: # pragma: no cover
  120. raise RuntimeError("Cannot add middleware after an application has started")
  121. self.user_middleware.insert(0, Middleware(middleware_class, *args, **kwargs))
  122. def add_exception_handler(
  123. self,
  124. exc_class_or_status_code: int | typing.Type[Exception],
  125. handler: ExceptionHandler,
  126. ) -> None: # pragma: no cover
  127. self.exception_handlers[exc_class_or_status_code] = handler
  128. def add_event_handler(
  129. self,
  130. event_type: str,
  131. func: typing.Callable, # type: ignore[type-arg]
  132. ) -> None: # pragma: no cover
  133. self.router.add_event_handler(event_type, func)
  134. def add_route(
  135. self,
  136. path: str,
  137. route: typing.Callable[[Request], typing.Awaitable[Response] | Response],
  138. methods: typing.Optional[typing.List[str]] = None,
  139. name: typing.Optional[str] = None,
  140. include_in_schema: bool = True,
  141. ) -> None: # pragma: no cover
  142. self.router.add_route(
  143. path, route, methods=methods, name=name, include_in_schema=include_in_schema
  144. )
  145. def add_websocket_route(
  146. self,
  147. path: str,
  148. route: typing.Callable[[WebSocket], typing.Awaitable[None]],
  149. name: str | None = None,
  150. ) -> None: # pragma: no cover
  151. self.router.add_websocket_route(path, route, name=name)
  152. def exception_handler(
  153. self, exc_class_or_status_code: int | typing.Type[Exception]
  154. ) -> typing.Callable: # type: ignore[type-arg]
  155. warnings.warn(
  156. "The `exception_handler` decorator is deprecated, and will be removed in version 1.0.0. " # noqa: E501
  157. "Refer to https://www.starlette.io/exceptions/ for the recommended approach.", # noqa: E501
  158. DeprecationWarning,
  159. )
  160. def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg] # noqa: E501
  161. self.add_exception_handler(exc_class_or_status_code, func)
  162. return func
  163. return decorator
  164. def route(
  165. self,
  166. path: str,
  167. methods: typing.List[str] | None = None,
  168. name: str | None = None,
  169. include_in_schema: bool = True,
  170. ) -> typing.Callable: # type: ignore[type-arg]
  171. """
  172. We no longer document this decorator style API, and its usage is discouraged.
  173. Instead you should use the following approach:
  174. >>> routes = [Route(path, endpoint=...), ...]
  175. >>> app = Starlette(routes=routes)
  176. """
  177. warnings.warn(
  178. "The `route` decorator is deprecated, and will be removed in version 1.0.0. " # noqa: E501
  179. "Refer to https://www.starlette.io/routing/ for the recommended approach.", # noqa: E501
  180. DeprecationWarning,
  181. )
  182. def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg] # noqa: E501
  183. self.router.add_route(
  184. path,
  185. func,
  186. methods=methods,
  187. name=name,
  188. include_in_schema=include_in_schema,
  189. )
  190. return func
  191. return decorator
  192. def websocket_route(self, path: str, name: str | None = None) -> typing.Callable: # type: ignore[type-arg]
  193. """
  194. We no longer document this decorator style API, and its usage is discouraged.
  195. Instead you should use the following approach:
  196. >>> routes = [WebSocketRoute(path, endpoint=...), ...]
  197. >>> app = Starlette(routes=routes)
  198. """
  199. warnings.warn(
  200. "The `websocket_route` decorator is deprecated, and will be removed in version 1.0.0. " # noqa: E501
  201. "Refer to https://www.starlette.io/routing/#websocket-routing for the recommended approach.", # noqa: E501
  202. DeprecationWarning,
  203. )
  204. def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg] # noqa: E501
  205. self.router.add_websocket_route(path, func, name=name)
  206. return func
  207. return decorator
  208. def middleware(self, middleware_type: str) -> typing.Callable: # type: ignore[type-arg] # noqa: E501
  209. """
  210. We no longer document this decorator style API, and its usage is discouraged.
  211. Instead you should use the following approach:
  212. >>> middleware = [Middleware(...), ...]
  213. >>> app = Starlette(middleware=middleware)
  214. """
  215. warnings.warn(
  216. "The `middleware` decorator is deprecated, and will be removed in version 1.0.0. " # noqa: E501
  217. "Refer to https://www.starlette.io/middleware/#using-middleware for recommended approach.", # noqa: E501
  218. DeprecationWarning,
  219. )
  220. assert (
  221. middleware_type == "http"
  222. ), 'Currently only middleware("http") is supported.'
  223. def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg] # noqa: E501
  224. self.add_middleware(BaseHTTPMiddleware, dispatch=func)
  225. return func
  226. return decorator