diff --git a/CHANGES/2550.bugfix b/CHANGES/2550.bugfix new file mode 100644 index 00000000000..de39a572ee6 --- /dev/null +++ b/CHANGES/2550.bugfix @@ -0,0 +1 @@ +Make `request.app` point to proper application instance when using nested applications (with middlewares). diff --git a/aiohttp/web.py b/aiohttp/web.py index 1e3115b1f7f..26bd5eaa6ce 100644 --- a/aiohttp/web.py +++ b/aiohttp/web.py @@ -22,6 +22,7 @@ from .web_exceptions import * # noqa from .web_fileresponse import * # noqa from .web_middlewares import * # noqa +from .web_middlewares import _fix_request_current_app from .web_protocol import * # noqa from .web_request import * # noqa from .web_response import * # noqa @@ -267,6 +268,8 @@ def _prepare_middleware(self): 'see #2252'.format(m), DeprecationWarning, stacklevel=2) yield m, False + if self._middlewares: + yield _fix_request_current_app(self), True async def _handle(self, request): match_info = await self._router.resolve(request) @@ -298,8 +301,9 @@ async def _handle(self, request): ("Handler {!r} should return response instance, " "got {!r} [middlewares {!r}]").format( match_info.handler, type(resp), - [middleware for middleware in app.middlewares - for app in match_info.apps]) + [middleware + for app in match_info.apps + for middleware in app.middlewares]) return resp def __call__(self): diff --git a/aiohttp/web_middlewares.py b/aiohttp/web_middlewares.py index 0266984fa2a..a85af8f821d 100644 --- a/aiohttp/web_middlewares.py +++ b/aiohttp/web_middlewares.py @@ -78,3 +78,12 @@ async def impl(request, handler): return await handler(request) return impl + + +def _fix_request_current_app(app): + + @middleware + async def impl(request, handler): + with request.match_info.set_current_app(app): + return await handler(request) + return impl diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index 76802f3bdd3..4f27aed474f 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -609,10 +609,10 @@ def match_info(self): """Result of route resolving.""" return self._match_info - @reify + @property def app(self): """Application instance.""" - return self._match_info.apps[-1] + return self._match_info.current_app async def _prepare_hook(self, response): match_info = self._match_info diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 48401db011f..64ea012d530 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -10,6 +10,7 @@ import warnings from collections import namedtuple from collections.abc import Container, Iterable, Sequence, Sized +from contextlib import contextmanager from functools import wraps from pathlib import Path from types import MappingProxyType @@ -170,6 +171,7 @@ def __init__(self, match_dict, route): super().__init__(match_dict) self._route = route self._apps = () + self._current_app = None self._frozen = False @property @@ -198,8 +200,26 @@ def apps(self): def add_app(self, app): if self._frozen: raise RuntimeError("Cannot change apps stack after .freeze() call") + if self._current_app is None: + self._current_app = app self._apps = (app,) + self._apps + @property + def current_app(self): + return self._current_app + + @contextmanager + def set_current_app(self, app): + assert app in self._apps, ( + "Expected one of the following apps {!r}, got {!r}" + .format(self._apps, app)) + prev = self._current_app + self._current_app = app + try: + yield + finally: + self._current_app = prev + def freeze(self): self._frozen = True diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index e05c7b34bd7..3b6ca32fc16 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -1290,6 +1290,41 @@ async def on_signal(app): assert [app, subapp1, subapp2] == order +@pytest.mark.parametrize('route,expected', [ + ('/sub/', ['app see root', 'subapp see sub']), + ('/', ['app see root']), +]) +async def test_subapp_middleware_context(loop, test_client, route, expected): + values = [] + + def show_app_context(appname): + @web.middleware + async def middleware(request, handler): + values.append('{} see {}'.format(appname, request.app['my_value'])) + return await handler(request) + return middleware + + async def handler(request): + return web.Response(text='Ok') + + app = web.Application() + app['my_value'] = 'root' + app.middlewares.append(show_app_context('app')) + app.router.add_get('/', handler) + + subapp = web.Application() + subapp['my_value'] = 'sub' + subapp.middlewares.append(show_app_context('subapp')) + subapp.router.add_get('/', handler) + app.add_subapp('/sub/', subapp) + + client = await test_client(app) + resp = await client.get(route) + assert 200 == resp.status + assert 'Ok' == await resp.text() + assert expected == values + + async def test_custom_date_header(loop, test_client): async def handler(request):