diff --git a/aiohttp/abc.py b/aiohttp/abc.py index bc20b27951d..8aec87734a8 100644 --- a/aiohttp/abc.py +++ b/aiohttp/abc.py @@ -35,6 +35,29 @@ def http_exception(self): def get_info(self): """Return a dict with additional info useful for introspection""" + @property # pragma: no branch + @abstractmethod + def apps(self): + """Stack of nested applications. + + Top level application is left-most element. + + """ + + @abstractmethod + def add_app(self, app): + """Add application to the nested apps stack.""" + + @abstractmethod + def freeze(self): + """Freeze the match info. + + The method is called after route resolution. + + After the call .add_app() is forbidden. + + """ + class AbstractView(ABC): diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py index 5806a93af08..14881d70f23 100644 --- a/aiohttp/test_utils.py +++ b/aiohttp/test_utils.py @@ -19,7 +19,7 @@ from .helpers import sentinel from .protocol import HttpVersion, RawRequestMessage from .signals import Signal -from .web import Application, Request +from .web import Application, Request, UrlMappingMatchInfo PY_35 = sys.version_info >= (3, 5) @@ -66,6 +66,7 @@ def start_server(self, **kwargs): self._root = URL('{}://{}:{}'.format(self.scheme, self.host, self.port)) + yield from self.app.startup() self.handler = self.app.make_handler(**kwargs) self.server = yield from self._loop.create_server(self.handler, self.host, @@ -502,10 +503,14 @@ def make_mocked_request(method, path, headers=None, *, if payload is sentinel: payload = mock.Mock() - req = Request(app, message, payload, + req = Request(message, payload, transport, reader, writer, secure_proxy_ssl_header=secure_proxy_ssl_header) + match_info = UrlMappingMatchInfo({}, mock.Mock()) + match_info.add_app(app) + req._match_info = match_info + return req diff --git a/aiohttp/web.py b/aiohttp/web.py index 1a5bf7ceffa..444ae39c704 100644 --- a/aiohttp/web.py +++ b/aiohttp/web.py @@ -39,7 +39,6 @@ def __init__(self, manager, app, router, *, self._manager = manager self._app = app self._router = router - self._middlewares = app.middlewares self._secure_proxy_ssl_header = secure_proxy_ssl_header def __repr__(self): @@ -65,15 +64,16 @@ def handle_request(self, message, payload): app = self._app request = web_reqrep.Request( - app, message, payload, + message, payload, self.transport, self.reader, self.writer, secure_proxy_ssl_header=self._secure_proxy_ssl_header) self._meth = request.method self._path = request.path try: match_info = yield from self._router.resolve(request) - assert isinstance(match_info, AbstractMatchInfo), match_info + match_info.add_app(app) + match_info.freeze() resp = None request._match_info = match_info @@ -84,7 +84,7 @@ def handle_request(self, message, payload): if resp is None: handler = match_info.handler - for factory in reversed(self._middlewares): + for factory in match_info.middlewares: handler = yield from factory(app, handler) resp = yield from handler(request) @@ -166,7 +166,7 @@ def __init__(self, *, logger=web_logger, loop=None, if loop is None: loop = asyncio.get_event_loop() if router is None: - router = web_urldispatcher.UrlDispatcher() + router = web_urldispatcher.UrlDispatcher(self) assert isinstance(router, AbstractRouter), router self._debug = debug @@ -188,6 +188,13 @@ def __init__(self, *, logger=web_logger, loop=None, def debug(self): return self._debug + def _reg_subapp_signals(self, subapp): + self._on_pre_signal.extend(subapp.on_pre_signal) + self._on_post_signal.extend(subapp.on_post_signal) + self._on_startup.extend(subapp.on_startup) + self._on_shutdown.extend(subapp.on_shutdown) + self._on_cleanup.extend(subapp.on_cleanup) + @property def on_response_prepare(self): return self._on_response_prepare @@ -288,7 +295,7 @@ def __call__(self): return self def __repr__(self): - return "" + return "".format(id(self)) def run_app(app, *, host='0.0.0.0', port=None, diff --git a/aiohttp/web_reqrep.py b/aiohttp/web_reqrep.py index 57d21ad8b9d..3859102c711 100644 --- a/aiohttp/web_reqrep.py +++ b/aiohttp/web_reqrep.py @@ -91,9 +91,9 @@ class Request(dict, HeadersMixin): POST_METHODS = {hdrs.METH_PATCH, hdrs.METH_POST, hdrs.METH_PUT, hdrs.METH_TRACE, hdrs.METH_DELETE} - def __init__(self, app, message, payload, transport, reader, writer, *, + def __init__(self, message, payload, transport, reader, writer, *, secure_proxy_ssl_header=None): - self._app = app + self._app = None self._message = message self._transport = transport self._reader = reader @@ -274,10 +274,10 @@ def match_info(self): """Result of route resolving.""" return self._match_info - @property + @reify def app(self): """Application instance.""" - return self._app + return self._match_info.apps[-1] @property def transport(self): @@ -724,7 +724,8 @@ def prepare(self, request): resp_impl = self._start_pre_check(request) if resp_impl is not None: return resp_impl - yield from request.app.on_response_prepare.send(request, self) + for app in request.match_info.apps: + yield from app.on_response_prepare.send(request, self) return self._start(request) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 4121892fcff..ba2c0db1ad3 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -59,11 +59,19 @@ def url_for(self, **kwargs): @asyncio.coroutine @abc.abstractmethod # pragma: no branch - def resolve(self, method, path): + def resolve(self, request): """Resolve resource Return (UrlMappingMatchInfo, allowed_methods) pair.""" + @abc.abstractmethod + def add_prefix(self, prefix): + """Add a prefix to processed URLs. + + Required for subapplications support. + + """ + @abc.abstractmethod def get_info(self): """Return a dict with additional info useful for introspection""" @@ -155,6 +163,8 @@ class UrlMappingMatchInfo(dict, AbstractMatchInfo): def __init__(self, match_dict, route): super().__init__(match_dict) self._route = route + self._apps = [] + self._frozen = False @property def handler(self): @@ -175,6 +185,25 @@ def http_exception(self): def get_info(self): return self._route.get_info() + @property + def apps(self): + return tuple(self._apps) + + @property + def middlewares(self): + middlewares = [] + for app in self._apps: + middlewares.extend(reversed(app.middlewares)) + return middlewares + + def add_app(self, app): + if self._frozen: + raise RuntimeError("Cannot change apps stack after .freeze() call") + self._apps.insert(0, app) + + def freeze(self): + self._frozen = True + def __repr__(self): return "".format(super().__repr__(), self._route) @@ -235,10 +264,10 @@ def register_route(self, route): self._routes.append(route) @asyncio.coroutine - def resolve(self, method, path): + def resolve(self, request): allowed_methods = set() - match_dict = self._match(path) + match_dict = self._match(request.rel_url.raw_path) if match_dict is None: return None, allowed_methods @@ -246,7 +275,7 @@ def resolve(self, method, path): route_method = route.method allowed_methods.add(route_method) - if route_method == method or route_method == hdrs.METH_ANY: + if route_method == request.method or route_method == hdrs.METH_ANY: return UrlMappingMatchInfo(match_dict, route), allowed_methods else: return None, allowed_methods @@ -262,8 +291,15 @@ class PlainResource(Resource): def __init__(self, path, *, name=None): super().__init__(name=name) + assert path.startswith('/') self._path = path + def add_prefix(self, prefix): + assert prefix.startswith('/') + assert prefix.endswith('/') + assert len(prefix) > 1 + self._path = prefix + self._path[1:] + def _match(self, path): # string comparison is about 10 times faster than regexp matching if self._path == path: @@ -291,11 +327,20 @@ class DynamicResource(Resource): def __init__(self, pattern, formatter, *, name=None): super().__init__(name=name) + assert pattern.pattern.startswith('\\/') + assert formatter.startswith('/') self._pattern = pattern self._formatter = formatter + def add_prefix(self, prefix): + assert prefix.startswith('/') + assert prefix.endswith('/') + assert len(prefix) > 1 + self._pattern = re.compile(re.escape(prefix)+self._pattern.pattern[2:]) + self._formatter = prefix + self._formatter[1:] + def _match(self, path): - match = self._pattern.match(path) + match = self._pattern.fullmatch(path) if match is None: return None else: @@ -329,6 +374,13 @@ def __init__(self, prefix, *, name=None): self._prefix = quote(prefix, safe='/') self._prefix_len = len(self._prefix) + def add_prefix(self, prefix): + assert prefix.startswith('/') + assert prefix.endswith('/') + assert len(prefix) > 1 + self._prefix = prefix + self._prefix[1:] + self._prefix_len = len(self._prefix) + class StaticResource(PrefixResource): @@ -375,7 +427,9 @@ def get_info(self): 'prefix': self._prefix} @asyncio.coroutine - def resolve(self, method, path): + def resolve(self, request): + path = request.rel_url.raw_path + method = request.method allowed_methods = {'GET', 'HEAD'} if not path.startswith(self._prefix): return None, set() @@ -468,6 +522,55 @@ def __repr__(self): name=name, path=self._prefix, directory=self._directory) +class PrefixedSubAppResource(PrefixResource): + + def __init__(self, prefix, app): + super().__init__(prefix) + self._app = app + for resource in app.router.resources(): + resource.add_prefix(prefix) + + def add_prefix(self, prefix): + super().add_prefix(prefix) + for resource in self._app.router.resources(): + resource.add_prefix(prefix) + + def url_for(self, *args, **kwargs): + raise RuntimeError(".url_for() is not supported " + "by sub-application root") + + def url(self, **kwargs): + """Construct url for route with additional params.""" + raise RuntimeError(".url() is not supported " + "by sub-application root") + + def get_info(self): + return {'app': self._app, + 'prefix': self._prefix} + + @asyncio.coroutine + def resolve(self, request): + if not request.url.raw_path.startswith(self._prefix): + return None, set() + match_info = yield from self._app.router.resolve(request) + match_info.add_app(self._app) + if isinstance(match_info.http_exception, HTTPMethodNotAllowed): + methods = match_info.http_exception.allowed_methods + else: + methods = set() + return (match_info, methods) + + def __len__(self): + return len(self._app.router.routes()) + + def __iter__(self): + return iter(self._app.router.routes()) + + def __repr__(self): + return " {app!r}>".format( + prefix=self._prefix, app=self._app) + + class ResourceRoute(AbstractRoute): """A route with resource""" @@ -590,26 +693,26 @@ def __contains__(self, route): class UrlDispatcher(AbstractRouter, collections.abc.Mapping): - DYN = re.compile(r'^\{(?P[a-zA-Z][_a-zA-Z0-9]*)\}$') + DYN = re.compile(r'\{(?P[a-zA-Z][_a-zA-Z0-9]*)\}') DYN_WITH_RE = re.compile( - r'^\{(?P[a-zA-Z][_a-zA-Z0-9]*):(?P.+)\}$') + r'\{(?P[a-zA-Z][_a-zA-Z0-9]*):(?P.+)\}') GOOD = r'[^{}/]+' ROUTE_RE = re.compile(r'(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})') NAME_SPLIT_RE = re.compile('[.:-]') - def __init__(self): + def __init__(self, app): super().__init__() self._resources = [] self._named_resources = {} + self._app = app @asyncio.coroutine def resolve(self, request): - path = request.raw_path method = request.method allowed_methods = set() for resource in self._resources: - match_dict, allowed = yield from resource.resolve(method, path) + match_dict, allowed = yield from resource.resolve(request) if match_dict is not None: return match_dict else: @@ -675,13 +778,13 @@ def add_resource(self, path, *, name=None): pattern = '' formatter = '' for part in self.ROUTE_RE.split(path): - match = self.DYN.match(part) + match = self.DYN.fullmatch(part) if match: pattern += '(?P<{}>{})'.format(match.group('var'), self.GOOD) formatter += '{' + match.group('var') + '}' continue - match = self.DYN_WITH_RE.match(part) + match = self.DYN_WITH_RE.fullmatch(part) if match: pattern += '(?P<{var}>{re})'.format(**match.groupdict()) formatter += '{' + match.group('var') + '}' @@ -695,7 +798,7 @@ def add_resource(self, path, *, name=None): pattern += re.escape(part) try: - compiled = re.compile('^' + pattern + '$') + compiled = re.compile(pattern) except re.error as exc: raise ValueError( "Bad pattern '{}': {}".format(pattern, exc)) from None @@ -767,3 +870,12 @@ def add_delete(self, *args, **kwargs): Shortcut for add_route with method DELETE """ return self.add_route(hdrs.METH_DELETE, *args, **kwargs) + + def add_subapp(self, prefix, subapp): + assert prefix.startswith('/') + if not prefix.endswith('/'): + prefix += '/' + resource = PrefixedSubAppResource(prefix, subapp) + self._reg_resource(resource) + self._app._reg_subapp_signals(subapp) + return resource diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 5ffe4ce3f8e..1457c3f91f2 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -664,7 +664,7 @@ StreamResponse Use :meth:`prepare` instead. .. warning:: The method doesn't call - :attr:`web.Application.on_response_prepare` signal, use + :attr:`~aiohttp.web.Application.on_response_prepare` signal, use :meth:`prepare` instead. .. coroutinemethod:: prepare(request) @@ -675,7 +675,7 @@ StreamResponse Send *HTTP header*. You should not change any header data after calling this method. - The coroutine calls :attr:`web.Application.on_response_prepare` + The coroutine calls :attr:`~aiohttp.web.Application.on_response_prepare` signal handlers. .. versionadded:: 0.18 diff --git a/docs/yarl_and_encoding.rst b/docs/yarl_and_encoding.rst index 83acb8967b4..a74d8db981a 100644 --- a/docs/yarl_and_encoding.rst +++ b/docs/yarl_and_encoding.rst @@ -57,3 +57,24 @@ are the same. Internally ``'/путь'`` is converted into percent-encoding representation. Route matching also accepts both URL forms: raw and encoded. + + +Sub-Applications +---------------- + +Add sub application to route:: + + subapp = web.Application() + # setup subapp routes and middlewares + + app.add_subapp('/prefix/', subapp) + +Middlewares and signals from ``app`` and ``subapp`` are chained. + +Application can be used either as main app (``app.make_handler()``) or as +sub-application -- not both cases at the same time. + +Url reversing for sub-applications should generate urls with proper prefix. + +``SubAppResource`` is a regular prefixed resource with single route for +sinking every request into sub-application. diff --git a/tests/test_signals.py b/tests/test_signals.py index b4719593ac5..c7288d60a5d 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -4,9 +4,9 @@ import pytest from multidict import CIMultiDict -from aiohttp.protocol import HttpVersion11, RawRequestMessage from aiohttp.signals import Signal -from aiohttp.web import Application, Request, Response +from aiohttp.test_utils import make_mocked_request +from aiohttp.web import Application, Response @pytest.fixture @@ -20,21 +20,7 @@ def debug_app(loop): def make_request(app, method, path, headers=CIMultiDict()): - message = RawRequestMessage(method, path, HttpVersion11, headers, - [(k.encode('utf-8'), v.encode('utf-8')) - for k, v in headers.items()], - False, False) - return request_from_message(message, app) - - -def request_from_message(message, app): - payload = mock.Mock() - transport = mock.Mock() - reader = mock.Mock() - writer = mock.Mock() - req = Request(app, message, payload, - transport, reader, writer) - return req + return make_mocked_request(method, path, headers, app=app) def test_add_response_prepare_signal_handler(loop, app): diff --git a/tests/test_urldispatch.py b/tests/test_urldispatch.py index 0613ca65cb1..971a3b020e7 100644 --- a/tests/test_urldispatch.py +++ b/tests/test_urldispatch.py @@ -8,11 +8,10 @@ import pytest from yarl import URL -import aiohttp.web -from aiohttp import hdrs +import aiohttp +from aiohttp import hdrs, web from aiohttp.test_utils import make_mocked_request -from aiohttp.web import (HTTPMethodNotAllowed, HTTPNotFound, Response, - UrlDispatcher) +from aiohttp.web import HTTPMethodNotAllowed, HTTPNotFound, Response from aiohttp.web_urldispatcher import (AbstractResource, ResourceRoute, SystemRoute, View, _defaultExpectHandler) @@ -32,8 +31,13 @@ def handler(request): @pytest.fixture -def router(): - return UrlDispatcher() +def app(loop): + return web.Application(loop=loop) + + +@pytest.fixture +def router(app): + return app.router @pytest.fixture @@ -377,7 +381,8 @@ def test_static_not_match(router): router.add_static('/pre', os.path.dirname(aiohttp.__file__), name='name') resource = router['name'] - ret = yield from resource.resolve('GET', '/another/path') + ret = yield from resource.resolve( + make_mocked_request('GET', '/another/path')) assert (None, set()) == ret @@ -781,10 +786,21 @@ def test_match_info_get_info_dynamic(router): req = make_request('GET', '/value') info = yield from router.resolve(req) assert info.get_info() == { - 'pattern': re.compile('^\\/(?P[^{}/]+)$'), + 'pattern': re.compile('\\/(?P[^{}/]+)'), 'formatter': '/{a}'} +@asyncio.coroutine +def test_match_info_get_info_dynamic2(router): + handler = make_handler() + router.add_route('GET', '/{a}/{b}', handler) + req = make_request('GET', '/path/to') + info = yield from router.resolve(req) + assert info.get_info() == { + 'pattern': re.compile('\\/(?P[^{}/]+)\\/(?P[^{}/]+)'), + 'formatter': '/{a}/{b}'} + + def test_static_resource_get_info(router): directory = pathlib.Path(aiohttp.__file__).parent resource = router.add_static('/st', directory) @@ -848,7 +864,8 @@ def test_static_route_points_to_file(router): def test_404_for_static_resource(router): resource = router.add_static('/st', os.path.dirname(aiohttp.__file__)) - ret = yield from resource.resolve('GET', '/unknown/path') + ret = yield from resource.resolve( + make_mocked_request('GET', '/unknown/path')) assert (None, set()) == ret @@ -856,7 +873,8 @@ def test_404_for_static_resource(router): def test_405_for_resource_adapter(router): resource = router.add_static('/st', os.path.dirname(aiohttp.__file__)) - ret = yield from resource.resolve('POST', '/st/abc.py') + ret = yield from resource.resolve( + make_mocked_request('POST', '/st/abc.py')) assert (None, {'HEAD', 'GET'}) == ret @@ -865,7 +883,7 @@ def test_check_allowed_method_for_found_resource(router): handler = make_handler() resource = router.add_resource('/') resource.add_route('GET', handler) - ret = yield from resource.resolve('GET', '/') + ret = yield from resource.resolve(make_mocked_request('GET', '/')) assert ret[0] is not None assert {'GET'} == ret[1] @@ -887,3 +905,51 @@ def test_url_for_in_resource_route(router): route = router.add_route('GET', '/get/{name}', make_handler(), name='name') assert URL('/get/John') == route.url_for(name='John') + + +def test_subapp_get_info(router, loop): + subapp = web.Application(loop=loop) + resource = router.add_subapp('/pre', subapp) + assert resource.get_info() == {'prefix': '/pre/', 'app': subapp} + + +def test_subapp_url(router, loop): + subapp = web.Application(loop=loop) + resource = router.add_subapp('/pre', subapp) + with pytest.raises(RuntimeError): + resource.url() + + +def test_subapp_url_for(router, loop): + subapp = web.Application(loop=loop) + resource = router.add_subapp('/pre', subapp) + with pytest.raises(RuntimeError): + resource.url_for() + + +def test_subapp_repr(router, loop): + subapp = web.Application(loop=loop) + resource = router.add_subapp('/pre', subapp) + assert repr(resource).startswith( + ' " == repr(app) + assert "".format(id(app)) == repr(app) @asyncio.coroutine @@ -859,3 +859,330 @@ def redirected(request): client = yield from test_client(app) resp = yield from client.get('/redirector') assert resp.status == 200 + + +@asyncio.coroutine +def test_simple_subapp(loop, test_client): + @asyncio.coroutine + def handler(request): + return web.Response(text="OK") + + app = web.Application(loop=loop) + subapp = web.Application(loop=loop) + subapp.router.add_get('/to', handler) + app.router.add_subapp('/path', subapp) + + client = yield from test_client(app) + resp = yield from client.get('/path/to') + assert resp.status == 200 + txt = yield from resp.text() + assert 'OK' == txt + + +@asyncio.coroutine +def test_subapp_reverse_url(loop, test_client): + @asyncio.coroutine + def handler(request): + return web.HTTPMovedPermanently( + location=subapp.router['name'].url_for()) + + @asyncio.coroutine + def handler2(request): + return web.Response(text="OK") + + app = web.Application(loop=loop) + subapp = web.Application(loop=loop) + subapp.router.add_get('/to', handler) + subapp.router.add_get('/final', handler2, name='name') + app.router.add_subapp('/path', subapp) + + client = yield from test_client(app) + resp = yield from client.get('/path/to') + assert resp.status == 200 + txt = yield from resp.text() + assert 'OK' == txt + assert resp.url_obj.path == '/path/final' + + +@asyncio.coroutine +def test_subapp_reverse_variable_url(loop, test_client): + @asyncio.coroutine + def handler(request): + return web.HTTPMovedPermanently( + location=subapp.router['name'].url_for(part='final')) + + @asyncio.coroutine + def handler2(request): + return web.Response(text="OK") + + app = web.Application(loop=loop) + subapp = web.Application(loop=loop) + subapp.router.add_get('/to', handler) + subapp.router.add_get('/{part}', handler2, name='name') + app.router.add_subapp('/path', subapp) + + client = yield from test_client(app) + resp = yield from client.get('/path/to') + assert resp.status == 200 + txt = yield from resp.text() + assert 'OK' == txt + assert resp.url_obj.path == '/path/final' + + +@asyncio.coroutine +def test_subapp_reverse_static_url(loop, test_client): + fname = 'software_development_in_picture.jpg' + + @asyncio.coroutine + def handler(request): + return web.HTTPMovedPermanently( + location=subapp.router['name'].url_for(filename=fname)) + + app = web.Application(loop=loop) + subapp = web.Application(loop=loop) + subapp.router.add_get('/to', handler) + here = pathlib.Path(__file__).parent + subapp.router.add_static('/static', here, name='name') + app.router.add_subapp('/path', subapp) + + client = yield from test_client(app) + resp = yield from client.get('/path/to') + assert resp.url_obj.path == '/path/static/' + fname + assert resp.status == 200 + body = yield from resp.read() + with (here / fname).open('rb') as f: + assert body == f.read() + + +@asyncio.coroutine +def test_subapp_app(loop, test_client): + @asyncio.coroutine + def handler(request): + assert request.app is subapp + return web.HTTPOk(text='OK') + + app = web.Application(loop=loop) + subapp = web.Application(loop=loop) + subapp.router.add_get('/to', handler) + app.router.add_subapp('/path/', subapp) + + client = yield from test_client(app) + resp = yield from client.get('/path/to') + assert resp.status == 200 + txt = yield from resp.text() + assert 'OK' == txt + + +@asyncio.coroutine +def test_subapp_not_found(loop, test_client): + @asyncio.coroutine + def handler(request): + return web.HTTPOk(text='OK') + + app = web.Application(loop=loop) + subapp = web.Application(loop=loop) + subapp.router.add_get('/to', handler) + app.router.add_subapp('/path/', subapp) + + client = yield from test_client(app) + resp = yield from client.get('/path/other') + assert resp.status == 404 + + +@asyncio.coroutine +def test_subapp_not_found2(loop, test_client): + @asyncio.coroutine + def handler(request): + return web.HTTPOk(text='OK') + + app = web.Application(loop=loop) + subapp = web.Application(loop=loop) + subapp.router.add_get('/to', handler) + app.router.add_subapp('/path/', subapp) + + client = yield from test_client(app) + resp = yield from client.get('/invalid/other') + assert resp.status == 404 + + +@asyncio.coroutine +def test_subapp_not_allowed(loop, test_client): + @asyncio.coroutine + def handler(request): + return web.HTTPOk(text='OK') + + app = web.Application(loop=loop) + subapp = web.Application(loop=loop) + subapp.router.add_get('/to', handler) + app.router.add_subapp('/path/', subapp) + + client = yield from test_client(app) + resp = yield from client.post('/path/to') + assert resp.status == 405 + assert resp.headers['Allow'] == 'GET' + + +@asyncio.coroutine +def test_subapp_cannot_add_app_in_handler(loop, test_client): + @asyncio.coroutine + def handler(request): + request.match_info.add_app(app) + return web.HTTPOk(text='OK') + + app = web.Application(loop=loop) + subapp = web.Application(loop=loop) + subapp.router.add_get('/to', handler) + app.router.add_subapp('/path/', subapp) + + client = yield from test_client(app) + resp = yield from client.get('/path/to') + assert resp.status == 500 + + +@asyncio.coroutine +def test_subapp_middlewares(loop, test_client): + order = [] + + @asyncio.coroutine + def handler(request): + return web.HTTPOk(text='OK') + + @asyncio.coroutine + def middleware_factory(app, handler): + + @asyncio.coroutine + def middleware(request): + order.append((1, app)) + resp = yield from handler(request) + assert 200 == resp.status + order.append((2, app)) + return resp + return middleware + + app = web.Application(loop=loop, middlewares=[middleware_factory]) + subapp1 = web.Application(loop=loop, middlewares=[middleware_factory]) + subapp2 = web.Application(loop=loop, middlewares=[middleware_factory]) + subapp2.router.add_get('/to', handler) + subapp1.router.add_subapp('/b/', subapp2) + app.router.add_subapp('/a/', subapp1) + + client = yield from test_client(app) + resp = yield from client.get('/a/b/to') + assert resp.status == 200 + assert [(1, app), (1, subapp1), (1, subapp2), + (2, subapp2), (2, subapp1), (2, app)] == order + + +@asyncio.coroutine +def test_subapp_on_response_prepare(loop, test_client): + order = [] + + @asyncio.coroutine + def handler(request): + return web.HTTPOk(text='OK') + + def make_signal(app): + + @asyncio.coroutine + def on_response(request, response): + order.append(app) + + return on_response + + app = web.Application(loop=loop) + app.on_response_prepare.append(make_signal(app)) + subapp1 = web.Application(loop=loop) + subapp1.on_response_prepare.append(make_signal(subapp1)) + subapp2 = web.Application(loop=loop) + subapp2.on_response_prepare.append(make_signal(subapp2)) + subapp2.router.add_get('/to', handler) + subapp1.router.add_subapp('/b/', subapp2) + app.router.add_subapp('/a/', subapp1) + + client = yield from test_client(app) + resp = yield from client.get('/a/b/to') + assert resp.status == 200 + assert [app, subapp1, subapp2] == order + + +@asyncio.coroutine +def test_subapp_on_startup(loop, test_server): + order = [] + + def make_signal(cur_app): + + @asyncio.coroutine + def on_signal(app2): + assert app2 is app + order.append(cur_app) + + return on_signal + + app = web.Application(loop=loop) + app.on_startup.append(make_signal(app)) + subapp1 = web.Application(loop=loop) + subapp1.on_startup.append(make_signal(subapp1)) + subapp2 = web.Application(loop=loop) + subapp2.on_startup.append(make_signal(subapp2)) + subapp1.router.add_subapp('/b/', subapp2) + app.router.add_subapp('/a/', subapp1) + + yield from test_server(app) + + assert [app, subapp1, subapp2] == order + + +@asyncio.coroutine +def test_subapp_on_shutdown(loop, test_server): + order = [] + + def make_signal(cur_app): + + @asyncio.coroutine + def on_signal(app2): + assert app2 is app + order.append(cur_app) + + return on_signal + + app = web.Application(loop=loop) + app.on_shutdown.append(make_signal(app)) + subapp1 = web.Application(loop=loop) + subapp1.on_shutdown.append(make_signal(subapp1)) + subapp2 = web.Application(loop=loop) + subapp2.on_shutdown.append(make_signal(subapp2)) + subapp1.router.add_subapp('/b/', subapp2) + app.router.add_subapp('/a/', subapp1) + + server = yield from test_server(app) + yield from server.close() + + assert [app, subapp1, subapp2] == order + + +@asyncio.coroutine +def test_subapp_on_cleanup(loop, test_server): + order = [] + + def make_signal(cur_app): + + @asyncio.coroutine + def on_signal(app2): + assert app2 is app + order.append(cur_app) + + return on_signal + + app = web.Application(loop=loop) + app.on_cleanup.append(make_signal(app)) + subapp1 = web.Application(loop=loop) + subapp1.on_cleanup.append(make_signal(subapp1)) + subapp2 = web.Application(loop=loop) + subapp2.on_cleanup.append(make_signal(subapp2)) + subapp1.router.add_subapp('/b/', subapp2) + app.router.add_subapp('/a/', subapp1) + + server = yield from test_server(app) + yield from server.close() + + assert [app, subapp1, subapp2] == order diff --git a/tests/test_web_request.py b/tests/test_web_request.py index 7771f478538..265660a8151 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -185,10 +185,7 @@ def test_request_cookie__set_item(make_request): def test_match_info(make_request): req = make_request('GET', '/') - assert req.match_info is None - match = {'a': 'b'} - req._match_info = match - assert match is req.match_info + assert req._match_info is req.match_info def test_request_is_dict(make_request): diff --git a/tests/test_web_response.py b/tests/test_web_response.py index c02e7296ad7..173cca39d59 100644 --- a/tests/test_web_response.py +++ b/tests/test_web_response.py @@ -8,32 +8,20 @@ from multidict import CIMultiDict from aiohttp import hdrs, signals -from aiohttp.protocol import (HttpVersion, HttpVersion10, HttpVersion11, - RawRequestMessage) -from aiohttp.web import (ContentCoding, Request, Response, StreamResponse, - json_response) +from aiohttp.protocol import HttpVersion, HttpVersion10, HttpVersion11 +from aiohttp.test_utils import make_mocked_request +from aiohttp.web import ContentCoding, Response, StreamResponse, json_response def make_request(method, path, headers=CIMultiDict(), version=HttpVersion11, **kwargs): - message = RawRequestMessage(method, path, version, headers, - [(k.encode('utf-8'), v.encode('utf-8')) - for k, v in headers.items()], - False, False) - return request_from_message(message, **kwargs) - - -def request_from_message(message, **kwargs): - app = kwargs.get('app') or mock.Mock() + app = kwargs.pop('app', None) or mock.Mock() app._debug = False app.on_response_prepare = signals.Signal(app) - payload = mock.Mock() - transport = mock.Mock() - reader = mock.Mock() - writer = kwargs.get('writer') or mock.Mock() - req = Request(app, message, payload, - transport, reader, writer) - return req + writer = kwargs.pop('writer', None) or mock.Mock() + return make_mocked_request(method, path, headers, + version=version, writer=writer, + app=app, **kwargs) def test_stream_response_ctor(): @@ -544,9 +532,7 @@ def test___repr__not_started(): @asyncio.coroutine def test_keep_alive_http10_default(): - message = RawRequestMessage('GET', '/', HttpVersion10, CIMultiDict(), - [], True, False) - req = request_from_message(message) + req = make_request('GET', '/', version=HttpVersion10) resp = StreamResponse() yield from resp.prepare(req) assert not resp.keep_alive @@ -555,22 +541,17 @@ def test_keep_alive_http10_default(): @asyncio.coroutine def test_keep_alive_http10_switched_on(): headers = CIMultiDict(Connection='keep-alive') - message = RawRequestMessage('GET', '/', HttpVersion10, headers, - [(b'Connection', b'keep-alive')], - False, False) - req = request_from_message(message) + req = make_request('GET', '/', version=HttpVersion10, headers=headers) + req._message = req._message._replace(should_close=False) resp = StreamResponse() yield from resp.prepare(req) - assert resp.keep_alive is True + assert resp.keep_alive @asyncio.coroutine def test_keep_alive_http09(): headers = CIMultiDict(Connection='keep-alive') - message = RawRequestMessage('GET', '/', HttpVersion(0, 9), headers, - [(b'Connection', b'keep-alive')], - False, False) - req = request_from_message(message) + req = make_request('GET', '/', version=HttpVersion(0, 9), headers=headers) resp = StreamResponse() yield from resp.prepare(req) assert not resp.keep_alive