import asyncio import warnings from collections import MutableMapping from functools import partial from typing import TYPE_CHECKING, Awaitable, Callable from . import hdrs from .abc import AbstractAccessLogger, AbstractMatchInfo, AbstractRouter from .frozenlist import FrozenList from .helpers import DEBUG, AccessLogger from .log import web_logger from .signals import Signal from .web_middlewares import _fix_request_current_app from .web_request import Request from .web_response import StreamResponse from .web_server import Server from .web_urldispatcher import PrefixedSubAppResource, UrlDispatcher __all__ = ('Application', 'CleanupError') if TYPE_CHECKING: # pragma: no branch _AppSignal = Signal[Callable[['Application'], Awaitable[None]]] _RespPrepareSignal = Signal[Callable[[Request, StreamResponse], Awaitable[None]]] else: # No type checker mode, skip types _AppSignal = Signal _RespPrepareSignal = Signal class Application(MutableMapping): ATTRS = frozenset([ 'logger', '_debug', '_router', '_loop', '_handler_args', '_middlewares', '_middlewares_handlers', '_run_middlewares', '_state', '_frozen', '_pre_frozen', '_subapps', '_on_response_prepare', '_on_startup', '_on_shutdown', '_on_cleanup', '_client_max_size', '_cleanup_ctx']) def __init__(self, *, logger=web_logger, router=None, middlewares=(), handler_args=None, client_max_size=1024**2, loop=None, debug=...): if router is None: router = UrlDispatcher() else: warnings.warn("router argument is deprecated", DeprecationWarning, stacklevel=2) assert isinstance(router, AbstractRouter), router if loop is not None: warnings.warn("loop argument is deprecated", DeprecationWarning, stacklevel=2) self._debug = debug self._router = router # type: UrlDispatcher self._loop = loop self._handler_args = handler_args self.logger = logger self._middlewares = FrozenList(middlewares) self._middlewares_handlers = None # initialized on freezing self._run_middlewares = None # initialized on freezing self._state = {} self._frozen = False self._pre_frozen = False self._subapps = [] self._on_response_prepare = Signal(self) # type: _RespPrepareSignal self._on_startup = Signal(self) # type: _AppSignal self._on_shutdown = Signal(self) # type: _AppSignal self._on_cleanup = Signal(self) # type: _AppSignal self._cleanup_ctx = CleanupContext() self._on_startup.append(self._cleanup_ctx._on_startup) self._on_cleanup.append(self._cleanup_ctx._on_cleanup) self._client_max_size = client_max_size def __init_subclass__(cls): warnings.warn("Inheritance class {} from web.Application " "is discouraged".format(cls.__name__), DeprecationWarning, stacklevel=2) if DEBUG: def __setattr__(self, name, val): if name not in self.ATTRS: warnings.warn("Setting custom web.Application.{} attribute " "is discouraged".format(name), DeprecationWarning, stacklevel=2) super().__setattr__(name, val) # MutableMapping API def __eq__(self, other): return self is other def __getitem__(self, key): return self._state[key] def _check_frozen(self): if self._frozen: warnings.warn("Changing state of started or joined " "application is deprecated", DeprecationWarning, stacklevel=3) def __setitem__(self, key, value): self._check_frozen() self._state[key] = value def __delitem__(self, key): self._check_frozen() del self._state[key] def __len__(self): return len(self._state) def __iter__(self): return iter(self._state) ######## @property def loop(self): return self._loop def _set_loop(self, loop): if loop is None: loop = asyncio.get_event_loop() if self._loop is not None and self._loop is not loop: raise RuntimeError( "web.Application instance initialized with different loop") self._loop = loop # set loop debug if self._debug is ...: self._debug = loop.get_debug() # set loop to sub applications for subapp in self._subapps: subapp._set_loop(loop) @property def pre_frozen(self) -> bool: return self._pre_frozen def pre_freeze(self) -> None: if self._pre_frozen: return self._pre_frozen = True self._middlewares.freeze() self._router.freeze() self._on_response_prepare.freeze() self._cleanup_ctx.freeze() self._on_startup.freeze() self._on_shutdown.freeze() self._on_cleanup.freeze() self._middlewares_handlers = tuple(self._prepare_middleware()) # If current app and any subapp do not have middlewares avoid run all # of the code footprint that it implies, which have a middleware # hardcoded per app that sets up the current_app attribute. If no # middlewares are configured the handler will receive the proper # current_app without needing all of this code. self._run_middlewares = True if self.middlewares else False for subapp in self._subapps: subapp.pre_freeze() self._run_middlewares =\ self._run_middlewares or subapp._run_middlewares @property def frozen(self) -> bool: return self._frozen def freeze(self) -> None: if self._frozen: return self.pre_freeze() self._frozen = True for subapp in self._subapps: subapp.freeze() @property def debug(self) -> bool: return self._debug def _reg_subapp_signals(self, subapp): def reg_handler(signame): subsig = getattr(subapp, signame) async def handler(app): await subsig.send(subapp) appsig = getattr(self, signame) appsig.append(handler) reg_handler('on_startup') reg_handler('on_shutdown') reg_handler('on_cleanup') def add_subapp(self, prefix: str, subapp: 'Application'): if self.frozen: raise RuntimeError( "Cannot add sub application to frozen application") if subapp.frozen: raise RuntimeError("Cannot add frozen application") if prefix.endswith('/'): prefix = prefix[:-1] if prefix in ('', '/'): raise ValueError("Prefix cannot be empty") resource = PrefixedSubAppResource(prefix, subapp) self.router.register_resource(resource) self._reg_subapp_signals(subapp) self._subapps.append(subapp) subapp.pre_freeze() if self._loop is not None: subapp._set_loop(self._loop) return resource def add_routes(self, routes): self.router.add_routes(routes) @property def on_response_prepare(self) -> _RespPrepareSignal: return self._on_response_prepare @property def on_startup(self) -> _AppSignal: return self._on_startup @property def on_shutdown(self) -> _AppSignal: return self._on_shutdown @property def on_cleanup(self) -> _AppSignal: return self._on_cleanup @property def cleanup_ctx(self): return self._cleanup_ctx @property def router(self) -> UrlDispatcher: return self._router @property def middlewares(self): return self._middlewares def _make_handler(self, *, loop=None, access_log_class=AccessLogger, **kwargs): if not issubclass(access_log_class, AbstractAccessLogger): raise TypeError( 'access_log_class must be subclass of ' 'aiohttp.abc.AbstractAccessLogger, got {}'.format( access_log_class)) self._set_loop(loop) self.freeze() kwargs['debug'] = self.debug if self._handler_args: for k, v in self._handler_args.items(): kwargs[k] = v return Server(self._handle, request_factory=self._make_request, access_log_class=access_log_class, loop=self.loop, **kwargs) def make_handler(self, *, loop=None, access_log_class=AccessLogger, **kwargs): warnings.warn("Application.make_handler(...) is deprecated, " "use AppRunner API instead", DeprecationWarning, stacklevel=2) return self._make_handler(loop=loop, access_log_class=access_log_class, **kwargs) async def startup(self) -> None: """Causes on_startup signal Should be called in the event loop along with the request handler. """ await self.on_startup.send(self) async def shutdown(self) -> None: """Causes on_shutdown signal Should be called before cleanup() """ await self.on_shutdown.send(self) async def cleanup(self) -> None: """Causes on_cleanup signal Should be called after shutdown() """ await self.on_cleanup.send(self) def _make_request(self, message, payload, protocol, writer, task, _cls=Request): return _cls( message, payload, protocol, writer, task, self._loop, client_max_size=self._client_max_size) def _prepare_middleware(self): for m in reversed(self._middlewares): if getattr(m, '__middleware_version__', None) == 1: yield m, True else: warnings.warn('old-style middleware "{!r}" deprecated, ' 'see #2252'.format(m), DeprecationWarning, stacklevel=2) yield m, False yield _fix_request_current_app(self), True async def _handle(self, request): match_info = await self._router.resolve(request) if DEBUG: # pragma: no cover if not isinstance(match_info, AbstractMatchInfo): raise TypeError("match_info should be AbstractMAtchInfo " "instance, not {!r}".format(match_info)) match_info.add_app(self) match_info.freeze() resp = None request._match_info = match_info expect = request.headers.get(hdrs.EXPECT) if expect: resp = await match_info.expect_handler(request) await request.writer.drain() if resp is None: handler = match_info.handler if self._run_middlewares: for app in match_info.apps[::-1]: for m, new_style in app._middlewares_handlers: if new_style: handler = partial(m, handler=handler) else: handler = await m(app, handler) resp = await handler(request) if DEBUG: if not isinstance(resp, StreamResponse): msg = ("Handler {!r} should return response instance, " "got {!r} [middlewares {!r}]").format( match_info.handler, type(resp), [middleware for app in match_info.apps for middleware in app.middlewares]) raise TypeError(msg) return resp def __call__(self): """gunicorn compatibility""" return self def __repr__(self): return "".format(id(self)) class CleanupError(RuntimeError): @property def exceptions(self): return self.args[1] class CleanupContext(FrozenList): def __init__(self): super().__init__() self._exits = [] async def _on_startup(self, app): for cb in self: it = cb(app).__aiter__() await it.__anext__() self._exits.append(it) async def _on_cleanup(self, app): errors = [] for it in reversed(self._exits): try: await it.__anext__() except StopAsyncIteration: pass except Exception as exc: errors.append(exc) else: errors.append(RuntimeError("{!r} has more than one 'yield'" .format(it))) if errors: if len(errors) == 1: raise errors[0] else: raise CleanupError("Multiple errors on cleanup stage", errors)