Feature: Routing overload
When user specifies HTTP methods to function handlers, it automatically will be overloaded unless they duplicate. Example: # This is a new route. It works as before. @app.route('/overload', methods=['GET']) async def handler1(request): return text('OK1') # This is the exiting route but a new method. They are merged and # work as combined. The route will serve all of GET, POST and PUT. @app.route('/overload', methods=['POST', 'PUT']) async def handler2(request): return text('OK2') # This is the existing route and PUT method is the duplicated method. # It raises RouteExists. @app.route('/overload', methods=['PUT', 'DELETE']) async def handler3(request): return text('Duplicated')
This commit is contained in:
parent
5a6fb679c9
commit
11f3c79a77
|
@ -3,6 +3,7 @@ from collections import defaultdict, namedtuple
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .exceptions import NotFound, InvalidUsage
|
from .exceptions import NotFound, InvalidUsage
|
||||||
|
from .views import CompositionView
|
||||||
|
|
||||||
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
|
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
|
||||||
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
||||||
|
@ -78,9 +79,6 @@ class Router:
|
||||||
self.hosts.add(host)
|
self.hosts.add(host)
|
||||||
uri = host + uri
|
uri = host + uri
|
||||||
|
|
||||||
if uri in self.routes_all:
|
|
||||||
raise RouteExists("Route already registered: {}".format(uri))
|
|
||||||
|
|
||||||
# Dict for faster lookups of if method allowed
|
# Dict for faster lookups of if method allowed
|
||||||
if methods:
|
if methods:
|
||||||
methods = frozenset(methods)
|
methods = frozenset(methods)
|
||||||
|
@ -113,9 +111,35 @@ class Router:
|
||||||
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
|
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
|
||||||
pattern = re.compile(r'^{}$'.format(pattern_string))
|
pattern = re.compile(r'^{}$'.format(pattern_string))
|
||||||
|
|
||||||
route = Route(
|
def merge_route(route, methods, handler):
|
||||||
handler=handler, methods=methods, pattern=pattern,
|
# merge to the existing route when possible.
|
||||||
parameters=parameters)
|
if not route.methods or not methods:
|
||||||
|
# method-unspecified routes are not mergeable.
|
||||||
|
raise RouteExists(
|
||||||
|
"Route already registered: {}".format(uri))
|
||||||
|
elif route.methods.intersection(methods):
|
||||||
|
# already existing method is not overloadable.
|
||||||
|
duplicated = methods.intersection(route.methods)
|
||||||
|
raise RouteExists(
|
||||||
|
"Route already registered: {} [{}]".format(
|
||||||
|
uri, ','.join(list(duplicated))))
|
||||||
|
if isinstance(route.handler, CompositionView):
|
||||||
|
view = route.handler
|
||||||
|
else:
|
||||||
|
view = CompositionView()
|
||||||
|
view.add(route.methods, route.handler)
|
||||||
|
view.add(methods, handler)
|
||||||
|
route = route._replace(
|
||||||
|
handler=view, methods=methods.union(route.methods))
|
||||||
|
return route
|
||||||
|
|
||||||
|
route = self.routes_all.get(uri)
|
||||||
|
if route:
|
||||||
|
route = merge_route(route, methods, handler)
|
||||||
|
else:
|
||||||
|
route = Route(
|
||||||
|
handler=handler, methods=methods, pattern=pattern,
|
||||||
|
parameters=parameters)
|
||||||
|
|
||||||
self.routes_all[uri] = route
|
self.routes_all[uri] = route
|
||||||
if properties['unhashable']:
|
if properties['unhashable']:
|
||||||
|
|
|
@ -61,3 +61,38 @@ class HTTPMethodView:
|
||||||
view.__doc__ = cls.__doc__
|
view.__doc__ = cls.__doc__
|
||||||
view.__module__ = cls.__module__
|
view.__module__ = cls.__module__
|
||||||
return view
|
return view
|
||||||
|
|
||||||
|
|
||||||
|
class CompositionView:
|
||||||
|
""" Simple method-function mapped view for the sanic.
|
||||||
|
You can add handler functions to methods (get, post, put, patch, delete)
|
||||||
|
for every HTTP method you want to support.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
view = CompositionView()
|
||||||
|
view.add(['GET'], lambda request: text('I am get method'))
|
||||||
|
view.add(['POST', 'PUT'], lambda request: text('I am post/put method'))
|
||||||
|
|
||||||
|
etc.
|
||||||
|
|
||||||
|
If someone tries to use a non-implemented method, there will be a
|
||||||
|
405 response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.handlers = {}
|
||||||
|
|
||||||
|
def add(self, methods, handler):
|
||||||
|
for method in methods:
|
||||||
|
if method in self.handlers:
|
||||||
|
raise KeyError(
|
||||||
|
'Method {} already is registered.'.format(method))
|
||||||
|
self.handlers[method] = handler
|
||||||
|
|
||||||
|
def __call__(self, request, *args, **kwargs):
|
||||||
|
handler = self.handlers.get(request.method.upper(), None)
|
||||||
|
if handler is None:
|
||||||
|
raise InvalidUsage(
|
||||||
|
'Method {} not allowed for URL {}'.format(
|
||||||
|
request.method, request.url), status_code=405)
|
||||||
|
return handler(request, *args, **kwargs)
|
||||||
|
|
|
@ -463,3 +463,67 @@ def test_remove_route_without_clean_cache():
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, uri='/test')
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_overload_routes():
|
||||||
|
app = Sanic('test_dynamic_route')
|
||||||
|
|
||||||
|
@app.route('/overload', methods=['GET'])
|
||||||
|
async def handler1(request):
|
||||||
|
return text('OK1')
|
||||||
|
|
||||||
|
@app.route('/overload', methods=['POST', 'PUT'])
|
||||||
|
async def handler2(request):
|
||||||
|
return text('OK2')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, 'get', uri='/overload')
|
||||||
|
assert response.text == 'OK1'
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, 'post', uri='/overload')
|
||||||
|
assert response.text == 'OK2'
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, 'put', uri='/overload')
|
||||||
|
assert response.text == 'OK2'
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, 'delete', uri='/overload')
|
||||||
|
assert response.status == 405
|
||||||
|
|
||||||
|
with pytest.raises(RouteExists):
|
||||||
|
@app.route('/overload', methods=['PUT', 'DELETE'])
|
||||||
|
async def handler3(request):
|
||||||
|
return text('Duplicated')
|
||||||
|
|
||||||
|
|
||||||
|
def test_unmergeable_overload_routes():
|
||||||
|
app = Sanic('test_dynamic_route')
|
||||||
|
|
||||||
|
@app.route('/overload_whole')
|
||||||
|
async def handler1(request):
|
||||||
|
return text('OK1')
|
||||||
|
|
||||||
|
with pytest.raises(RouteExists):
|
||||||
|
@app.route('/overload_whole', methods=['POST', 'PUT'])
|
||||||
|
async def handler2(request):
|
||||||
|
return text('Duplicated')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, 'get', uri='/overload_whole')
|
||||||
|
assert response.text == 'OK1'
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, 'post', uri='/overload_whole')
|
||||||
|
assert response.text == 'OK1'
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/overload_part', methods=['GET'])
|
||||||
|
async def handler1(request):
|
||||||
|
return text('OK1')
|
||||||
|
|
||||||
|
with pytest.raises(RouteExists):
|
||||||
|
@app.route('/overload_part')
|
||||||
|
async def handler2(request):
|
||||||
|
return text('Duplicated')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, 'get', uri='/overload_part')
|
||||||
|
assert response.text == 'OK1'
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, 'post', uri='/overload_part')
|
||||||
|
assert response.status == 405
|
||||||
|
|
Loading…
Reference in New Issue
Block a user