rebase
This commit is contained in:
parent
48aa51b739
commit
d614823013
|
@ -4,8 +4,7 @@ from functools import lru_cache
|
||||||
from .exceptions import NotFound, InvalidUsage
|
from .exceptions import NotFound, InvalidUsage
|
||||||
from .views import CompositionView
|
from .views import CompositionView
|
||||||
|
|
||||||
Route = namedtuple(
|
Route = namedtuple('Route',
|
||||||
'Route',
|
|
||||||
['handler', 'methods', 'pattern', 'parameters', 'name'])
|
['handler', 'methods', 'pattern', 'parameters', 'name'])
|
||||||
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
||||||
|
|
||||||
|
@ -70,6 +69,28 @@ class Router:
|
||||||
self.routes_always_check = []
|
self.routes_always_check = []
|
||||||
self.hosts = None
|
self.hosts = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""
|
||||||
|
The typical user inspecting the router will likely want to see
|
||||||
|
the routes available. Provide a simple representation.
|
||||||
|
"""
|
||||||
|
def _route_to_str(uri, route):
|
||||||
|
out = 'name={0.name}, methods={0.methods}, URI={1}>\n'.format(
|
||||||
|
route, uri)
|
||||||
|
|
||||||
|
if route.handler.__doc__:
|
||||||
|
out += '{}\n'.format(route.handler.__doc__)
|
||||||
|
|
||||||
|
out += '\n'
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
out = ''
|
||||||
|
for uri, route in self.routes_all.items():
|
||||||
|
out += _route_to_str(uri, route)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
def parse_parameter_string(self, parameter_string):
|
def parse_parameter_string(self, parameter_string):
|
||||||
"""
|
"""
|
||||||
Parse a parameter string into its constituent name, type, and pattern
|
Parse a parameter string into its constituent name, type, and pattern
|
||||||
|
@ -130,11 +151,16 @@ class Router:
|
||||||
properties = {"unhashable": None}
|
properties = {"unhashable": None}
|
||||||
|
|
||||||
def add_parameter(match):
|
def add_parameter(match):
|
||||||
|
# We could receive NAME or NAME:PATTERN
|
||||||
name = match.group(1)
|
name = match.group(1)
|
||||||
name, _type, pattern = self.parse_parameter_string(name)
|
pattern = 'string'
|
||||||
|
if ':' in name:
|
||||||
|
name, pattern = name.split(':', 1)
|
||||||
|
|
||||||
parameter = Parameter(
|
default = (str, pattern)
|
||||||
name=name, cast=_type)
|
# Pull from pre-configured types
|
||||||
|
_type, pattern = REGEX_TYPES.get(pattern, default)
|
||||||
|
parameter = Parameter(name=name, cast=_type)
|
||||||
parameters.append(parameter)
|
parameters.append(parameter)
|
||||||
|
|
||||||
# Mark the whole route as unhashable if it has the hash key in it
|
# Mark the whole route as unhashable if it has the hash key in it
|
||||||
|
@ -146,7 +172,7 @@ class Router:
|
||||||
|
|
||||||
return '({})'.format(pattern)
|
return '({})'.format(pattern)
|
||||||
|
|
||||||
pattern_string = re.sub(self.parameter_pattern, 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))
|
||||||
|
|
||||||
def merge_route(route, methods, handler):
|
def merge_route(route, methods, handler):
|
||||||
|
|
|
@ -17,6 +17,7 @@ from .response import HTTPResponse
|
||||||
from .router import Router
|
from .router import Router
|
||||||
from .server import serve, serve_multiple, HttpProtocol
|
from .server import serve, serve_multiple, HttpProtocol
|
||||||
from .static import register as static_register
|
from .static import register as static_register
|
||||||
|
from .views import CompositionView
|
||||||
|
|
||||||
|
|
||||||
class Sanic:
|
class Sanic:
|
||||||
|
@ -120,7 +121,16 @@ class Sanic:
|
||||||
"""
|
"""
|
||||||
# Handle HTTPMethodView differently
|
# Handle HTTPMethodView differently
|
||||||
if hasattr(handler, 'view_class'):
|
if hasattr(handler, 'view_class'):
|
||||||
methods = frozenset(HTTP_METHODS)
|
methods = set()
|
||||||
|
|
||||||
|
for method in HTTP_METHODS:
|
||||||
|
if getattr(handler.view_class, method.lower(), None):
|
||||||
|
methods.add(method)
|
||||||
|
|
||||||
|
# handle composition view differently
|
||||||
|
if isinstance(handler, CompositionView):
|
||||||
|
methods = handler.handlers.keys()
|
||||||
|
|
||||||
self.route(uri=uri, methods=methods, host=host)(handler)
|
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from .exceptions import InvalidUsage
|
from .exceptions import InvalidUsage
|
||||||
|
from .constants import HTTP_METHODS
|
||||||
|
|
||||||
|
|
||||||
class HTTPMethodView:
|
class HTTPMethodView:
|
||||||
|
@ -40,11 +41,7 @@ class HTTPMethodView:
|
||||||
|
|
||||||
def dispatch_request(self, request, *args, **kwargs):
|
def dispatch_request(self, request, *args, **kwargs):
|
||||||
handler = getattr(self, request.method.lower(), None)
|
handler = getattr(self, request.method.lower(), None)
|
||||||
if handler:
|
return handler(request, *args, **kwargs)
|
||||||
return handler(request, *args, **kwargs)
|
|
||||||
raise InvalidUsage(
|
|
||||||
'Method {} not allowed for URL {}'.format(
|
|
||||||
request.method, request.url), status_code=405)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_view(cls, *class_args, **class_kwargs):
|
def as_view(cls, *class_args, **class_kwargs):
|
||||||
|
@ -89,15 +86,15 @@ class CompositionView:
|
||||||
|
|
||||||
def add(self, methods, handler):
|
def add(self, methods, handler):
|
||||||
for method in methods:
|
for method in methods:
|
||||||
|
if method not in HTTP_METHODS:
|
||||||
|
raise InvalidUsage(
|
||||||
|
'{} is not a valid HTTP method.'.format(method))
|
||||||
|
|
||||||
if method in self.handlers:
|
if method in self.handlers:
|
||||||
raise KeyError(
|
raise InvalidUsage(
|
||||||
'Method {} already is registered.'.format(method))
|
'Method {} is already registered.'.format(method))
|
||||||
self.handlers[method] = handler
|
self.handlers[method] = handler
|
||||||
|
|
||||||
def __call__(self, request, *args, **kwargs):
|
def __call__(self, request, *args, **kwargs):
|
||||||
handler = self.handlers.get(request.method.upper(), None)
|
handler = self.handlers[request.method.upper()]
|
||||||
if handler is None:
|
|
||||||
raise InvalidUsage(
|
|
||||||
'Method {} not allowed for URL {}'.format(
|
|
||||||
request.method, request.url), status_code=405)
|
|
||||||
return handler(request, *args, **kwargs)
|
return handler(request, *args, **kwargs)
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import pytest as pytest
|
import pytest as pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
from sanic.exceptions import InvalidUsage
|
||||||
from sanic.response import text, HTTPResponse
|
from sanic.response import text, HTTPResponse
|
||||||
from sanic.views import HTTPMethodView
|
from sanic.views import HTTPMethodView, CompositionView
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
@ -196,3 +197,56 @@ def test_with_decorator():
|
||||||
request, response = sanic_endpoint_test(app, method="get")
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
assert response.text == 'I am get method'
|
assert response.text == 'I am get method'
|
||||||
assert results[0] == 1
|
assert results[0] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_composition_view_rejects_incorrect_methods():
|
||||||
|
def foo(request):
|
||||||
|
return text('Foo')
|
||||||
|
|
||||||
|
view = CompositionView()
|
||||||
|
|
||||||
|
with pytest.raises(InvalidUsage) as e:
|
||||||
|
view.add(['GET', 'FOO'], foo)
|
||||||
|
|
||||||
|
assert str(e.value) == 'FOO is not a valid HTTP method.'
|
||||||
|
|
||||||
|
|
||||||
|
def test_composition_view_rejects_duplicate_methods():
|
||||||
|
def foo(request):
|
||||||
|
return text('Foo')
|
||||||
|
|
||||||
|
view = CompositionView()
|
||||||
|
|
||||||
|
with pytest.raises(InvalidUsage) as e:
|
||||||
|
view.add(['GET', 'POST', 'GET'], foo)
|
||||||
|
|
||||||
|
assert str(e.value) == 'Method GET is already registered.'
|
||||||
|
|
||||||
|
|
||||||
|
def test_composition_view_runs_methods_as_expected():
|
||||||
|
app = Sanic('test_composition_view')
|
||||||
|
|
||||||
|
view = CompositionView()
|
||||||
|
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))
|
||||||
|
view.add(['DELETE', 'PATCH'], lambda x: text('second method'))
|
||||||
|
|
||||||
|
app.add_route(view, '/')
|
||||||
|
|
||||||
|
for method in ['GET', 'POST', 'PUT']:
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/', method=method)
|
||||||
|
assert response.text == 'first method'
|
||||||
|
|
||||||
|
for method in ['DELETE', 'PATCH']:
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/', method=method)
|
||||||
|
assert response.text == 'second method'
|
||||||
|
|
||||||
|
def test_composition_view_rejects_invalid_methods():
|
||||||
|
app = Sanic('test_composition_view')
|
||||||
|
|
||||||
|
view = CompositionView()
|
||||||
|
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))
|
||||||
|
|
||||||
|
app.add_route(view, '/')
|
||||||
|
for method in ['DELETE', 'PATCH']:
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/', method=method)
|
||||||
|
assert response.status == 405
|
||||||
|
|
Loading…
Reference in New Issue
Block a user