Merge branch 'master' into ssl

This commit is contained in:
Raphael Deem 2017-01-19 11:23:21 -08:00 committed by GitHub
commit d2217b5c5f
8 changed files with 175 additions and 13 deletions

View File

@ -11,9 +11,16 @@ from sanic.blueprints import Blueprint
app = Sanic() app = Sanic()
bp = Blueprint("bp", host="bp.example.com") bp = Blueprint("bp", host="bp.example.com")
@app.route('/', host=["example.com",
"somethingelse.com",
"therestofyourdomains.com"])
async def hello(request):
return text("Some defaults")
@app.route('/', host="example.com") @app.route('/', host="example.com")
async def hello(request): async def hello(request):
return text("Answer") return text("Answer")
@app.route('/', host="sub.example.com") @app.route('/', host="sub.example.com")
async def hello(request): async def hello(request):
return text("42") return text("42")

View File

@ -173,6 +173,7 @@ class Handler:
try: try:
response = handler(request=request, exception=exception) response = handler(request=request, exception=exception)
except: except:
log.error(format_exc())
if self.sanic.debug: if self.sanic.debug:
response_message = ( response_message = (
'Exception raised in exception handler "{}" ' 'Exception raised in exception handler "{}" '
@ -185,6 +186,7 @@ class Handler:
return response return response
def default(self, request, exception): def default(self, request, exception):
log.error(format_exc())
if issubclass(type(exception), SanicException): if issubclass(type(exception), SanicException):
return text( return text(
'Error: {}'.format(exception), 'Error: {}'.format(exception),

View File

@ -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'])
@ -75,11 +76,15 @@ class Router:
if self.hosts is None: if self.hosts is None:
self.hosts = set(host) self.hosts = set(host)
else: else:
if isinstance(host, list):
host = set(host)
self.hosts.add(host) self.hosts.add(host)
if isinstance(host, str):
uri = host + uri uri = host + uri
else:
if uri in self.routes_all: for h in host:
raise RouteExists("Route already registered: {}".format(uri)) self.add(uri, methods, handler, h)
return
# Dict for faster lookups of if method allowed # Dict for faster lookups of if method allowed
if methods: if methods:
@ -113,6 +118,32 @@ 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))
def merge_route(route, methods, handler):
# merge to the existing route when possible.
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( route = Route(
handler=handler, methods=methods, pattern=pattern, handler=handler, methods=methods, pattern=pattern,
parameters=parameters) parameters=parameters)

View File

@ -232,7 +232,7 @@ class Sanic:
e, format_exc())) e, format_exc()))
else: else:
response = HTTPResponse( response = HTTPResponse(
"An error occured while handling an error") "An error occurred while handling an error")
response_callback(response) response_callback(response)
@ -241,10 +241,10 @@ class Sanic:
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
after_start=None, before_stop=None, after_stop=None, ssl=None, after_start=None, before_stop=None, after_stop=None, ssl=None,
sock=None, workers=1, loop=None, protocol=HttpProtocol, sock=None, workers=1, loop=None, protocol=HttpProtocol,
backlog=100, stop_event=None): backlog=100, stop_event=None):
""" """
Runs the HTTP Server and listens until keyboard interrupt or term Runs the HTTP Server and listens until keyboard interrupt or term
signal. On termination, drains connections before closing. signal. On termination, drains connections before closing.

View File

@ -1,4 +1,5 @@
import asyncio import asyncio
import traceback
from functools import partial from functools import partial
from inspect import isawaitable from inspect import isawaitable
from signal import SIGINT, SIGTERM from signal import SIGINT, SIGTERM
@ -189,6 +190,12 @@ class HttpProtocol(asyncio.Protocol):
"Writing error failed, connection closed {}".format(e)) "Writing error failed, connection closed {}".format(e))
def bail_out(self, message): def bail_out(self, message):
if self.transport.is_closing():
log.error(
"Connection closed before error was sent to user @ {}".format(
self.transport.get_extra_info('peername')))
log.debug('Error experienced:\n{}'.format(traceback.format_exc()))
else:
exception = ServerError(message) exception = ServerError(message)
self.write_error(exception) self.write_error(exception)
log.error(message) log.error(message)

View File

@ -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)

View File

@ -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

View File

@ -4,7 +4,7 @@ from sanic.utils import sanic_endpoint_test
def test_vhosts(): def test_vhosts():
app = Sanic('test_text') app = Sanic('test_vhosts')
@app.route('/', host="example.com") @app.route('/', host="example.com")
async def handler(request): async def handler(request):
@ -21,3 +21,19 @@ def test_vhosts():
headers = {"Host": "subdomain.example.com"} headers = {"Host": "subdomain.example.com"}
request, response = sanic_endpoint_test(app, headers=headers) request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "You're at subdomain.example.com!" assert response.text == "You're at subdomain.example.com!"
def test_vhosts_with_list():
app = Sanic('test_vhosts')
@app.route('/', host=["hello.com", "world.com"])
async def handler(request):
return text("Hello, world!")
headers = {"Host": "hello.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "Hello, world!"
headers = {"Host": "world.com"}
request, response = sanic_endpoint_test(app, headers=headers)
assert response.text == "Hello, world!"