From a91c9c10aeb9663c59d99445723ef55b6cca907f Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Sun, 21 Feb 2016 01:23:25 +0300 Subject: [PATCH] Support aiohttp 0.21.2 --- muffin/app.py | 26 ++++--- muffin/handler.py | 7 +- muffin/pytest.py | 3 +- muffin/urls.py | 159 ++++++++++++++++++++++++++++------------- muffin/worker.py | 7 +- requirements-tests.txt | 1 + tests/test_handlers.py | 18 ++--- tests/test_urls.py | 45 ++++++++---- 8 files changed, 175 insertions(+), 91 deletions(-) diff --git a/muffin/app.py b/muffin/app.py index e331b7a7..b966f0cb 100644 --- a/muffin/app.py +++ b/muffin/app.py @@ -1,7 +1,6 @@ """Implement Muffin Application.""" import logging.config import os -import re from asyncio import coroutine, iscoroutine, Future from importlib import import_module from inspect import isfunction, isclass, ismethod @@ -13,13 +12,10 @@ from muffin import CONFIGURATION_ENVIRON_VARIABLE from muffin.handler import Handler from muffin.manage import Manager -from muffin.urls import StaticRoute +from muffin.urls import StaticRoute, StaticResource from muffin.utils import LStruct, to_coroutine -RETYPE = type(re.compile('@')) - - class MuffinException(Exception): """Exception class for Muffin errors.""" @@ -73,6 +69,7 @@ def __init__(self, name, *, loop=None, router=None, middlewares=(), logger=web.w # Overide options self.defaults['CONFIG'] = OPTIONS.pop('CONFIG', self.defaults['CONFIG']) self.cfg.update(OPTIONS) + self._debug = self.cfg.DEBUG # Setup logging ch = logging.StreamHandler() @@ -82,10 +79,15 @@ def __init__(self, name, *, loop=None, router=None, middlewares=(), logger=web.w self.logger.name = 'muffin' self.logger.propagate = False - self.manage = Manager(self) + self.access_logger = access_logger + LOGGING_CFG = self.cfg.get('LOGGING') + if LOGGING_CFG and isinstance(LOGGING_CFG, dict): + logging.config.dictConfig(LOGGING_CFG) + # Setup CLI + self.manage = Manager(self) - # Setup static files option + # Setup static files if isinstance(self.cfg.STATIC_FOLDERS, str): self.cfg.STATIC_FOLDERS = [self.cfg.STATIC_FOLDERS] @@ -101,12 +103,6 @@ def __init__(self, name, *, loop=None, router=None, middlewares=(), logger=web.w self.logger.error('Plugin is invalid: %s', plugin) self.logger.exception(exc) - # Setup Logging - self.access_logger = access_logger - LOGGING_CFG = self.cfg.get('LOGGING') - if LOGGING_CFG and isinstance(LOGGING_CFG, dict): - logging.config.dictConfig(LOGGING_CFG) - def __repr__(self): """Human readable representation.""" return "" % self.name @@ -179,7 +175,9 @@ def start(self): for path in self.cfg.STATIC_FOLDERS: if os.path.isdir(path): route = StaticRoute(None, self.cfg.STATIC_PREFIX.rstrip('/') + '/', path) - self.router.register_route(route) + # TODO: Remove me when aiohttp > 0.21.2 will be relased. See #794 + resource = StaticResource(route) + self.router._reg_resource(resource) else: self.logger.warn('Disable static folder (hasnt found): %s', path) diff --git a/muffin/handler.py b/muffin/handler.py index 2b9f03ab..39d98d9f 100644 --- a/muffin/handler.py +++ b/muffin/handler.py @@ -1,11 +1,12 @@ """Base handler class.""" import inspect +import functools from asyncio import coroutine, iscoroutine import ujson as json from aiohttp.hdrs import METH_ANY from aiohttp.multidict import MultiDict, MultiDictProxy -from aiohttp.web import StreamResponse, HTTPMethodNotAllowed, Response +from aiohttp.web import StreamResponse, HTTPMethodNotAllowed, Response, View from muffin.urls import routes_register from muffin.utils import to_coroutine, abcoroutine @@ -116,10 +117,12 @@ def connect(cls, app, *paths, methods=None, name=None, router=None, view=None): if not hasattr(m, ROUTE_PARAMS_ATTR): continue paths_, methods_, name_ = getattr(m, ROUTE_PARAMS_ATTR) + name_ = name_ or ("%s.%s" % (cls.name, m.__name__)) delattr(m, ROUTE_PARAMS_ATTR) cls.app.register(*paths_, methods=methods_, name=name_, handler=cls)(m) @coroutine + @functools.wraps(cls) def handler(request): return cls().dispatch(request, view=view) @@ -147,6 +150,8 @@ def dispatch(self, request, view=None, **kwargs): return (yield from self.make_response(request, response)) + __iter__ = dispatch + @abcoroutine def make_response(self, request, response): """Convert a handler result to web response.""" diff --git a/muffin/pytest.py b/muffin/pytest.py index bf69c877..9dd76c46 100644 --- a/muffin/pytest.py +++ b/muffin/pytest.py @@ -76,7 +76,8 @@ def handle(environ, start_response): req = webob.Request(environ) vers = aiohttp.HttpVersion10 if req.http_version == 'HTTP/1.0' else aiohttp.HttpVersion11 message = aiohttp.RawRequestMessage( - req.method, req.path_qs, vers, aiohttp.CIMultiDict(req.headers), False, False) + req.method, req.path_qs, vers, aiohttp.CIMultiDict(req.headers), + req.headers, False, False) payload = aiohttp.StreamReader(loop=loop_) payload.feed_data(req.body) payload.feed_eof() diff --git a/muffin/urls.py b/muffin/urls.py index 03186df3..e9cbe1b0 100644 --- a/muffin/urls.py +++ b/muffin/urls.py @@ -1,11 +1,12 @@ """URL helpers.""" import re -from os import path as ospath +import asyncio from random import choice from string import printable +from urllib.parse import unquote -from aiohttp import web from aiohttp.hdrs import METH_ANY +from aiohttp.web import AbstractRoute, Resource, StaticRoute as VanilaStaticRoute, UrlDispatcher DYNS_RE = re.compile(r'(\{[^{}]*\})') @@ -13,22 +14,26 @@ RETYPE = type(re.compile('@')) -class RawReRoute(web.DynamicRoute): +class RawReResource(Resource): - """Support raw regular expresssions.""" + """Allow any regexp in routes.""" - def __init__(self, method, handler, name, pattern, *, expect_handler=None): - """Skip a formatter.""" + def __init__(self, pattern, name=None): + """Ensure that the pattern is regexp.""" if isinstance(pattern, str): pattern = re.compile(pattern) - super().__init__(method, handler, name, pattern, None, expect_handler=expect_handler) + self._pattern = pattern + super(RawReResource, self).__init__(name=name) - def match(self, path): - """Match given path.""" + def get_info(self): + """Get the resource's information.""" + return {'name': self._name, 'pattern': self._pattern} + + def _match(self, path): match = self._pattern.match(path) if match is None: return None - return match.groupdict('') + return {key: unquote(value) for key, value in match.groupdict('').items()} def url(self, *subgroups, query=None, **groups): """Build URL.""" @@ -45,12 +50,73 @@ def url(self, *subgroups, query=None, **groups): def __repr__(self): """Fix representation.""" - name = "'" + self.name + "' " if self.name is not None else "" - return " {handler!r}".format( - name=name, method=self.method, pattern=self._pattern, handler=self.handler) + return "" % (self.name or '', self._pattern) + + +# TODO: Remove me when aiohttp > 0.21.2 will be relased. See #794 +class StaticResource(Resource): + + def __init__(self, route): + super(StaticResource, self).__init__() + + assert isinstance(route, AbstractRoute), \ + 'Instance of Route class is required, got {!r}'.format(route) + self._route = route + self._routes.append(route) + + def url(self, **kwargs): + return self._route.url(**kwargs) + + def get_info(self): + return self._route.get_info() + + def _match(self, path): + return self._route.match(path) + + def __len__(self): + return 1 + + def __iter__(self): + yield self._route + +class ParentResource(Resource): -class StaticRoute(web.StaticRoute): + def __init__(self, path, *, name=None): + super(ParentResource, self).__init__(name=name) + self._path = path.rstrip('/') + self.router = UrlDispatcher() + + @asyncio.coroutine + def resolve(self, method, path): + allowed_methods = set() + if not path.startswith(self._path + '/'): + return None, allowed_methods + + path = path[len(self._path):] + + for resource in self.router._resources: + match_dict, allowed = yield from resource.resolve(method, path) + if match_dict is not None: + return match_dict, allowed_methods + else: + allowed_methods |= allowed + return None, allowed_methods + + def add_resource(self, path, *, name=None): + """Add resource.""" + return self.router.add_resource(path, name=name) + + def get_info(self): + return {'path': self._path} + + def url(self, name=None, **kwargs): + if name: + return self._path + self.router[name].url(**kwargs) + return self._path + '/' + + +class StaticRoute(VanilaStaticRoute): """Support multiple static resorces.""" @@ -60,56 +126,53 @@ def match(self, path): return None filename = path[self._prefix_len:] - filepath = ospath.join(self._directory, filename) - if not ospath.isfile(filepath): + try: + self._directory.joinpath(filename).resolve() + return {'filename': filename} + except (ValueError, FileNotFoundError): return None - return {'filename': path[self._prefix_len:]} - -def routes_register(app, view, *paths, methods=None, router=None, name=''): +def routes_register(app, handler, *paths, methods=None, router=None, name=None): """Register routes.""" if router is None: router = app.router - methods = methods or [METH_ANY] - routes = [] + resources = [] - for method in methods: - for path in paths: + for path in paths: - # Register any exception to app - if isinstance(path, type) and issubclass(path, Exception): - app._error_handlers[path] = view - continue + # Register any exception to app + if isinstance(path, type) and issubclass(path, Exception): + app._error_handlers[path] = handler + continue - num = 1 - cname = name + # Ensure that names are unique + name = str(name or '') + rname, rnum = name, 2 + while rname in router: + rname = "%s%d" % (rname, rnum) - # Ensure that the route's name is unique - if cname in router: - method_ = method.lower().replace('*', 'any') - cname, num = name + "." + method_, 1 - while cname in router: - cname = "%s%d.%s" % (name, num, method_) - num += 1 + path = parse(path) + if isinstance(path, RETYPE): + resource = RawReResource(path, name=rname) + router._reg_resource(resource) - # Is the path a regexp? - path = parse(path) + else: + resource = router.add_resource(path, name=rname) - # Support regex as path - if isinstance(path, RETYPE): - routes.append(router.register_route(RawReRoute(method.upper(), view, cname, path))) - continue - - # Support custom methods + for method in methods or [METH_ANY]: method = method.upper() - if method not in router.METHODS: - router.METHODS.add(method) - routes.append(router.add_route(method, path, view, name=cname)) + # Muffin allows to use any method + if method not in AbstractRoute.METHODS: + AbstractRoute.METHODS.add(method) + + resource.add_route(method, handler) + + resources.append(resource) - return routes + return resources def parse(path): diff --git a/muffin/worker.py b/muffin/worker.py index c5df4333..e50fdfe8 100644 --- a/muffin/worker.py +++ b/muffin/worker.py @@ -76,10 +76,9 @@ class GunicornWorker(GunicornWebWorker): def run(self): """Create asyncio server and start the loop.""" - app = self.app.callable - self.loop.set_debug(app.cfg.DEBUG) - app._loop = self.loop - self.loop.run_until_complete(app.start()) + self.loop.set_debug(self.wsgi.cfg.DEBUG) + self.wsgi._loop = self.loop + self.loop.run_until_complete(self.wsgi.start()) super(GunicornWorker, self).run() def make_handler(self, app, *args): diff --git a/requirements-tests.txt b/requirements-tests.txt index 4c7bb1cb..23ef59d1 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,6 +3,7 @@ mixer == 5.4.1 pytest == 2.8.7 +pytest-sugar == 0.5.1 pytest-pythonpath == 0.7 webtest == 2.0.20 diff --git a/tests/test_handlers.py b/tests/test_handlers.py index cc65fe9a..9d143c42 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -32,10 +32,9 @@ def rama(self, request): assert set(Resource.methods) == set(['GET', 'POST']) assert asyncio.iscoroutinefunction(Resource.get) - assert 'resource.any' in app.router._routes - - assert 'lama.post' in app.router - assert 'lama.patch' in app.router + assert 'resource' in app.router + assert 'resource.lama' in app.router + assert 'resource.rama' in app.router response = client.get('/res/lama/rama') assert response.text == 'LAMA' @@ -93,7 +92,8 @@ def test_handler_func(app, client): @app.register('/test') def test(request): return 'TEST PASSED' - assert 'test' in app.router._routes + + assert 'test' in app.router response = client.get('/test') assert response.text == 'TEST PASSED' @@ -105,7 +105,7 @@ def test(request): def test1(request): return 'TEST PASSED' - assert 'test1' in app.router._routes + assert 'test1' in app.router response = client.get('/test1') assert response.text == 'TEST PASSED' @@ -114,14 +114,14 @@ def test1(request): def test2(request): return 'TEST PASSED' - assert 'test2' in app.router._routes - assert 'test2.post' in app.router._routes + assert 'test2' in app.router @app.register('/test3', methods='*') def test3(request): return 'TEST PASSED' - assert 'test3' in app.router._routes + assert 'test3' in app.router + response = client.get('/test3') assert response.status_code == 200 diff --git a/tests/test_urls.py b/tests/test_urls.py index 9e0144f7..93e62038 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -2,22 +2,18 @@ def test_raw_route(): - from muffin.urls import RawReRoute + from muffin.urls import RawReResource - @asyncio.coroutine - def handler(request): - return 'OK' - - route = RawReRoute('GET', handler, 'test', '/foo/bar/?') - assert route.url() == '/foo/bar' + resource = RawReResource('/foo/bar/?', 'test') + assert resource.url() == '/foo/bar' - route = RawReRoute('GET', handler, 'test', '/foo/(?P\d+)(/(?P\w+))?/?') - assert route.url() == '/foo/0' - assert route.url(10) == '/foo/10' - assert route.url(bar=11) == '/foo/11' - assert route.url(foo=12) == '/foo/0/12' - assert route.url(bar=13, foo=14) == '/foo/13/14' - assert route.url(bar=13, foo=14, query={'a': 'b'}) == '/foo/13/14?a=b' + resource = RawReResource('/foo/(?P\d+)(/(?P\w+))?/?', 'test') + assert resource.url() == '/foo/0' + assert resource.url(10) == '/foo/10' + assert resource.url(bar=11) == '/foo/11' + assert resource.url(foo=12) == '/foo/0/12' + assert resource.url(bar=13, foo=14) == '/foo/13/14' + assert resource.url(bar=13, foo=14, query={'a': 'b'}) == '/foo/13/14?a=b' def test_parse(): @@ -28,3 +24,24 @@ def test_parse(): assert isinstance(parse('/{foo}/'), str) assert isinstance(parse('/{foo:\d+}/'), RETYPE) assert isinstance(parse('/{foo}/?'), RETYPE) + + +def test_parent(loop): + from muffin.urls import ParentResource + + parent = ParentResource('/api/', name='api') + resource = parent.add_resource('/test/', name='test') + + @asyncio.coroutine + def handler(request): + return 'TEST PASSED.' + + resource.add_route('*', handler) + info, _ = loop.run_until_complete(parent.resolve('GET', '/api/test/')) + assert info is not None + + result = loop.run_until_complete(info.handler(None)) + assert result == 'TEST PASSED.' + + assert parent.url() == '/api/' + assert parent.url(name='test') == '/api/test/'