Skip to content

Commit

Permalink
Support aiohttp 0.21.2
Browse files Browse the repository at this point in the history
  • Loading branch information
klen committed Feb 20, 2016
1 parent 30bbc54 commit a91c9c1
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 91 deletions.
26 changes: 12 additions & 14 deletions 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
Expand All @@ -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."""
Expand Down Expand Up @@ -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()
Expand All @@ -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]

Expand All @@ -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 "<Application: %s>" % self.name
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion 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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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."""
Expand Down
3 changes: 2 additions & 1 deletion muffin/pytest.py
Expand Up @@ -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()
Expand Down
159 changes: 111 additions & 48 deletions muffin/urls.py
@@ -1,34 +1,39 @@
"""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'(\{[^{}]*\})')
DYNR_RE = re.compile(r'^\{(?P<var>[a-zA-Z][_a-zA-Z0-9]*)(?::(?P<re>.+))*\}$')
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."""
Expand All @@ -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 "<RawReRoute {name}[{method}] {pattern} -> {handler!r}".format(
name=name, method=self.method, pattern=self._pattern, handler=self.handler)
return "<RawReResource '%s' %s>" % (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."""

Expand All @@ -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):
Expand Down
7 changes: 3 additions & 4 deletions muffin/worker.py
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions requirements-tests.txt
Expand Up @@ -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

Expand Down

0 comments on commit a91c9c1

Please sign in to comment.