You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

421 lines
13 KiB

4 years ago
  1. import asyncio
  2. import warnings
  3. from collections import MutableMapping
  4. from functools import partial
  5. from typing import TYPE_CHECKING, Awaitable, Callable
  6. from . import hdrs
  7. from .abc import AbstractAccessLogger, AbstractMatchInfo, AbstractRouter
  8. from .frozenlist import FrozenList
  9. from .helpers import DEBUG, AccessLogger
  10. from .log import web_logger
  11. from .signals import Signal
  12. from .web_middlewares import _fix_request_current_app
  13. from .web_request import Request
  14. from .web_response import StreamResponse
  15. from .web_server import Server
  16. from .web_urldispatcher import PrefixedSubAppResource, UrlDispatcher
  17. __all__ = ('Application', 'CleanupError')
  18. if TYPE_CHECKING: # pragma: no branch
  19. _AppSignal = Signal[Callable[['Application'], Awaitable[None]]]
  20. _RespPrepareSignal = Signal[Callable[[Request, StreamResponse],
  21. Awaitable[None]]]
  22. else:
  23. # No type checker mode, skip types
  24. _AppSignal = Signal
  25. _RespPrepareSignal = Signal
  26. class Application(MutableMapping):
  27. ATTRS = frozenset([
  28. 'logger', '_debug', '_router', '_loop', '_handler_args',
  29. '_middlewares', '_middlewares_handlers', '_run_middlewares',
  30. '_state', '_frozen', '_pre_frozen', '_subapps',
  31. '_on_response_prepare', '_on_startup', '_on_shutdown',
  32. '_on_cleanup', '_client_max_size', '_cleanup_ctx'])
  33. def __init__(self, *,
  34. logger=web_logger,
  35. router=None,
  36. middlewares=(),
  37. handler_args=None,
  38. client_max_size=1024**2,
  39. loop=None,
  40. debug=...):
  41. if router is None:
  42. router = UrlDispatcher()
  43. else:
  44. warnings.warn("router argument is deprecated", DeprecationWarning,
  45. stacklevel=2)
  46. assert isinstance(router, AbstractRouter), router
  47. if loop is not None:
  48. warnings.warn("loop argument is deprecated", DeprecationWarning,
  49. stacklevel=2)
  50. self._debug = debug
  51. self._router = router # type: UrlDispatcher
  52. self._loop = loop
  53. self._handler_args = handler_args
  54. self.logger = logger
  55. self._middlewares = FrozenList(middlewares)
  56. self._middlewares_handlers = None # initialized on freezing
  57. self._run_middlewares = None # initialized on freezing
  58. self._state = {}
  59. self._frozen = False
  60. self._pre_frozen = False
  61. self._subapps = []
  62. self._on_response_prepare = Signal(self) # type: _RespPrepareSignal
  63. self._on_startup = Signal(self) # type: _AppSignal
  64. self._on_shutdown = Signal(self) # type: _AppSignal
  65. self._on_cleanup = Signal(self) # type: _AppSignal
  66. self._cleanup_ctx = CleanupContext()
  67. self._on_startup.append(self._cleanup_ctx._on_startup)
  68. self._on_cleanup.append(self._cleanup_ctx._on_cleanup)
  69. self._client_max_size = client_max_size
  70. def __init_subclass__(cls):
  71. warnings.warn("Inheritance class {} from web.Application "
  72. "is discouraged".format(cls.__name__),
  73. DeprecationWarning,
  74. stacklevel=2)
  75. if DEBUG:
  76. def __setattr__(self, name, val):
  77. if name not in self.ATTRS:
  78. warnings.warn("Setting custom web.Application.{} attribute "
  79. "is discouraged".format(name),
  80. DeprecationWarning,
  81. stacklevel=2)
  82. super().__setattr__(name, val)
  83. # MutableMapping API
  84. def __eq__(self, other):
  85. return self is other
  86. def __getitem__(self, key):
  87. return self._state[key]
  88. def _check_frozen(self):
  89. if self._frozen:
  90. warnings.warn("Changing state of started or joined "
  91. "application is deprecated",
  92. DeprecationWarning,
  93. stacklevel=3)
  94. def __setitem__(self, key, value):
  95. self._check_frozen()
  96. self._state[key] = value
  97. def __delitem__(self, key):
  98. self._check_frozen()
  99. del self._state[key]
  100. def __len__(self):
  101. return len(self._state)
  102. def __iter__(self):
  103. return iter(self._state)
  104. ########
  105. @property
  106. def loop(self):
  107. return self._loop
  108. def _set_loop(self, loop):
  109. if loop is None:
  110. loop = asyncio.get_event_loop()
  111. if self._loop is not None and self._loop is not loop:
  112. raise RuntimeError(
  113. "web.Application instance initialized with different loop")
  114. self._loop = loop
  115. # set loop debug
  116. if self._debug is ...:
  117. self._debug = loop.get_debug()
  118. # set loop to sub applications
  119. for subapp in self._subapps:
  120. subapp._set_loop(loop)
  121. @property
  122. def pre_frozen(self) -> bool:
  123. return self._pre_frozen
  124. def pre_freeze(self) -> None:
  125. if self._pre_frozen:
  126. return
  127. self._pre_frozen = True
  128. self._middlewares.freeze()
  129. self._router.freeze()
  130. self._on_response_prepare.freeze()
  131. self._cleanup_ctx.freeze()
  132. self._on_startup.freeze()
  133. self._on_shutdown.freeze()
  134. self._on_cleanup.freeze()
  135. self._middlewares_handlers = tuple(self._prepare_middleware())
  136. # If current app and any subapp do not have middlewares avoid run all
  137. # of the code footprint that it implies, which have a middleware
  138. # hardcoded per app that sets up the current_app attribute. If no
  139. # middlewares are configured the handler will receive the proper
  140. # current_app without needing all of this code.
  141. self._run_middlewares = True if self.middlewares else False
  142. for subapp in self._subapps:
  143. subapp.pre_freeze()
  144. self._run_middlewares =\
  145. self._run_middlewares or subapp._run_middlewares
  146. @property
  147. def frozen(self) -> bool:
  148. return self._frozen
  149. def freeze(self) -> None:
  150. if self._frozen:
  151. return
  152. self.pre_freeze()
  153. self._frozen = True
  154. for subapp in self._subapps:
  155. subapp.freeze()
  156. @property
  157. def debug(self) -> bool:
  158. return self._debug
  159. def _reg_subapp_signals(self, subapp):
  160. def reg_handler(signame):
  161. subsig = getattr(subapp, signame)
  162. async def handler(app):
  163. await subsig.send(subapp)
  164. appsig = getattr(self, signame)
  165. appsig.append(handler)
  166. reg_handler('on_startup')
  167. reg_handler('on_shutdown')
  168. reg_handler('on_cleanup')
  169. def add_subapp(self, prefix: str, subapp: 'Application'):
  170. if self.frozen:
  171. raise RuntimeError(
  172. "Cannot add sub application to frozen application")
  173. if subapp.frozen:
  174. raise RuntimeError("Cannot add frozen application")
  175. if prefix.endswith('/'):
  176. prefix = prefix[:-1]
  177. if prefix in ('', '/'):
  178. raise ValueError("Prefix cannot be empty")
  179. resource = PrefixedSubAppResource(prefix, subapp)
  180. self.router.register_resource(resource)
  181. self._reg_subapp_signals(subapp)
  182. self._subapps.append(subapp)
  183. subapp.pre_freeze()
  184. if self._loop is not None:
  185. subapp._set_loop(self._loop)
  186. return resource
  187. def add_routes(self, routes):
  188. self.router.add_routes(routes)
  189. @property
  190. def on_response_prepare(self) -> _RespPrepareSignal:
  191. return self._on_response_prepare
  192. @property
  193. def on_startup(self) -> _AppSignal:
  194. return self._on_startup
  195. @property
  196. def on_shutdown(self) -> _AppSignal:
  197. return self._on_shutdown
  198. @property
  199. def on_cleanup(self) -> _AppSignal:
  200. return self._on_cleanup
  201. @property
  202. def cleanup_ctx(self):
  203. return self._cleanup_ctx
  204. @property
  205. def router(self) -> UrlDispatcher:
  206. return self._router
  207. @property
  208. def middlewares(self):
  209. return self._middlewares
  210. def _make_handler(self, *,
  211. loop=None,
  212. access_log_class=AccessLogger,
  213. **kwargs):
  214. if not issubclass(access_log_class, AbstractAccessLogger):
  215. raise TypeError(
  216. 'access_log_class must be subclass of '
  217. 'aiohttp.abc.AbstractAccessLogger, got {}'.format(
  218. access_log_class))
  219. self._set_loop(loop)
  220. self.freeze()
  221. kwargs['debug'] = self.debug
  222. if self._handler_args:
  223. for k, v in self._handler_args.items():
  224. kwargs[k] = v
  225. return Server(self._handle, request_factory=self._make_request,
  226. access_log_class=access_log_class,
  227. loop=self.loop, **kwargs)
  228. def make_handler(self, *,
  229. loop=None,
  230. access_log_class=AccessLogger,
  231. **kwargs):
  232. warnings.warn("Application.make_handler(...) is deprecated, "
  233. "use AppRunner API instead",
  234. DeprecationWarning,
  235. stacklevel=2)
  236. return self._make_handler(loop=loop,
  237. access_log_class=access_log_class,
  238. **kwargs)
  239. async def startup(self) -> None:
  240. """Causes on_startup signal
  241. Should be called in the event loop along with the request handler.
  242. """
  243. await self.on_startup.send(self)
  244. async def shutdown(self) -> None:
  245. """Causes on_shutdown signal
  246. Should be called before cleanup()
  247. """
  248. await self.on_shutdown.send(self)
  249. async def cleanup(self) -> None:
  250. """Causes on_cleanup signal
  251. Should be called after shutdown()
  252. """
  253. await self.on_cleanup.send(self)
  254. def _make_request(self, message, payload, protocol, writer, task,
  255. _cls=Request):
  256. return _cls(
  257. message, payload, protocol, writer, task,
  258. self._loop,
  259. client_max_size=self._client_max_size)
  260. def _prepare_middleware(self):
  261. for m in reversed(self._middlewares):
  262. if getattr(m, '__middleware_version__', None) == 1:
  263. yield m, True
  264. else:
  265. warnings.warn('old-style middleware "{!r}" deprecated, '
  266. 'see #2252'.format(m),
  267. DeprecationWarning, stacklevel=2)
  268. yield m, False
  269. yield _fix_request_current_app(self), True
  270. async def _handle(self, request):
  271. match_info = await self._router.resolve(request)
  272. if DEBUG: # pragma: no cover
  273. if not isinstance(match_info, AbstractMatchInfo):
  274. raise TypeError("match_info should be AbstractMAtchInfo "
  275. "instance, not {!r}".format(match_info))
  276. match_info.add_app(self)
  277. match_info.freeze()
  278. resp = None
  279. request._match_info = match_info
  280. expect = request.headers.get(hdrs.EXPECT)
  281. if expect:
  282. resp = await match_info.expect_handler(request)
  283. await request.writer.drain()
  284. if resp is None:
  285. handler = match_info.handler
  286. if self._run_middlewares:
  287. for app in match_info.apps[::-1]:
  288. for m, new_style in app._middlewares_handlers:
  289. if new_style:
  290. handler = partial(m, handler=handler)
  291. else:
  292. handler = await m(app, handler)
  293. resp = await handler(request)
  294. if DEBUG:
  295. if not isinstance(resp, StreamResponse):
  296. msg = ("Handler {!r} should return response instance, "
  297. "got {!r} [middlewares {!r}]").format(
  298. match_info.handler, type(resp),
  299. [middleware
  300. for app in match_info.apps
  301. for middleware in app.middlewares])
  302. raise TypeError(msg)
  303. return resp
  304. def __call__(self):
  305. """gunicorn compatibility"""
  306. return self
  307. def __repr__(self):
  308. return "<Application 0x{:x}>".format(id(self))
  309. class CleanupError(RuntimeError):
  310. @property
  311. def exceptions(self):
  312. return self.args[1]
  313. class CleanupContext(FrozenList):
  314. def __init__(self):
  315. super().__init__()
  316. self._exits = []
  317. async def _on_startup(self, app):
  318. for cb in self:
  319. it = cb(app).__aiter__()
  320. await it.__anext__()
  321. self._exits.append(it)
  322. async def _on_cleanup(self, app):
  323. errors = []
  324. for it in reversed(self._exits):
  325. try:
  326. await it.__anext__()
  327. except StopAsyncIteration:
  328. pass
  329. except Exception as exc:
  330. errors.append(exc)
  331. else:
  332. errors.append(RuntimeError("{!r} has more than one 'yield'"
  333. .format(it)))
  334. if errors:
  335. if len(errors) == 1:
  336. raise errors[0]
  337. else:
  338. raise CleanupError("Multiple errors on cleanup stage", errors)