Merge branch 'master' into sphinx-docs

This commit is contained in:
Cadel Watson 2017-01-20 09:30:42 +11:00
commit 7c4ffa8866
11 changed files with 237 additions and 18 deletions

View File

@ -56,6 +56,21 @@ Hello World Example
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000) app.run(host="0.0.0.0", port=8000)
SSL Example
-----------
Optionally pass in an SSLContext:
.. code:: python
import ssl
certificate = "/path/to/certificate"
keyfile = "/path/to/keyfile"
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certificate, keyfile=keyfile)
app.run(host="0.0.0.0", port=8443, ssl=context)
Installation Installation
------------ ------------

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

@ -3,6 +3,7 @@ from collections import defaultdict
class BlueprintSetup: class BlueprintSetup:
""" """
Creates a blueprint state like object.
""" """
def __init__(self, blueprint, app, options): def __init__(self, blueprint, app, options):
@ -32,13 +33,13 @@ class BlueprintSetup:
def add_exception(self, handler, *args, **kwargs): def add_exception(self, handler, *args, **kwargs):
""" """
Registers exceptions to sanic Registers exceptions to sanic.
""" """
self.app.exception(*args, **kwargs)(handler) self.app.exception(*args, **kwargs)(handler)
def add_static(self, uri, file_or_directory, *args, **kwargs): def add_static(self, uri, file_or_directory, *args, **kwargs):
""" """
Registers static files to sanic Registers static files to sanic.
""" """
if self.url_prefix: if self.url_prefix:
uri = self.url_prefix + uri uri = self.url_prefix + uri
@ -47,7 +48,7 @@ class BlueprintSetup:
def add_middleware(self, middleware, *args, **kwargs): def add_middleware(self, middleware, *args, **kwargs):
""" """
Registers middleware to sanic Registers middleware to sanic.
""" """
if args or kwargs: if args or kwargs:
self.app.middleware(*args, **kwargs)(middleware) self.app.middleware(*args, **kwargs)(middleware)
@ -77,11 +78,13 @@ class Blueprint:
def make_setup_state(self, app, options): def make_setup_state(self, app, options):
""" """
Returns a new BlueprintSetup object
""" """
return BlueprintSetup(self, app, options) return BlueprintSetup(self, app, options)
def register(self, app, options): def register(self, app, options):
""" """
Registers the blueprint to the sanic app.
""" """
state = self.make_setup_state(app, options) state = self.make_setup_state(app, options)
for deferred in self.deferred_functions: for deferred in self.deferred_functions:
@ -89,6 +92,9 @@ class Blueprint:
def route(self, uri, methods=None, host=None): def route(self, uri, methods=None, host=None):
""" """
Creates a blueprint route from a decorated function.
:param uri: Endpoint at which the route will be accessible.
:param methods: List of acceptable HTTP methods.
""" """
def decorator(handler): def decorator(handler):
self.record(lambda s: s.add_route(handler, uri, methods, host)) self.record(lambda s: s.add_route(handler, uri, methods, host))
@ -97,12 +103,18 @@ class Blueprint:
def add_route(self, handler, uri, methods=None, host=None): def add_route(self, handler, uri, methods=None, host=None):
""" """
Creates a blueprint route from a function.
:param handler: Function to handle uri request.
:param uri: Endpoint at which the route will be accessible.
:param methods: List of acceptable HTTP methods.
""" """
self.record(lambda s: s.add_route(handler, uri, methods, host)) self.record(lambda s: s.add_route(handler, uri, methods, host))
return handler return handler
def listener(self, event): def listener(self, event):
""" """
Create a listener from a decorated function.
:param event: Event to listen to.
""" """
def decorator(listener): def decorator(listener):
self.listeners[event].append(listener) self.listeners[event].append(listener)
@ -111,6 +123,7 @@ class Blueprint:
def middleware(self, *args, **kwargs): def middleware(self, *args, **kwargs):
""" """
Creates a blueprint middleware from a decorated function.
""" """
def register_middleware(middleware): def register_middleware(middleware):
self.record( self.record(
@ -127,6 +140,7 @@ class Blueprint:
def exception(self, *args, **kwargs): def exception(self, *args, **kwargs):
""" """
Creates a blueprint exception from a decorated function.
""" """
def decorator(handler): def decorator(handler):
self.record(lambda s: s.add_exception(handler, *args, **kwargs)) self.record(lambda s: s.add_exception(handler, *args, **kwargs))
@ -135,6 +149,9 @@ class Blueprint:
def static(self, uri, file_or_directory, *args, **kwargs): def static(self, uri, file_or_directory, *args, **kwargs):
""" """
Creates a blueprint static route from a decorated function.
:param uri: Endpoint at which the route will be accessible.
:param file_or_directory: Static asset.
""" """
self.record( self.record(
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs)) lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))

View File

@ -174,6 +174,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 "{}" '
@ -186,6 +187,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

@ -143,21 +143,45 @@ class HTTPResponse:
def json(body, status=200, headers=None): def json(body, status=200, headers=None):
"""
Returns response object with body in json format.
:param body: Response data to be serialized.
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(json_dumps(body), headers=headers, status=status, return HTTPResponse(json_dumps(body), headers=headers, status=status,
content_type="application/json") content_type="application/json")
def text(body, status=200, headers=None): def text(body, status=200, headers=None):
"""
Returns response object with body in text format.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(body, status=status, headers=headers, return HTTPResponse(body, status=status, headers=headers,
content_type="text/plain; charset=utf-8") content_type="text/plain; charset=utf-8")
def html(body, status=200, headers=None): def html(body, status=200, headers=None):
"""
Returns response object with body in html format.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(body, status=status, headers=headers, return HTTPResponse(body, status=status, headers=headers,
content_type="text/html; charset=utf-8") content_type="text/html; charset=utf-8")
async def file(location, mime_type=None, headers=None): async def file(location, mime_type=None, headers=None):
"""
Returns response object with file data.
:param location: Location of file on system.
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
"""
filename = path.split(location)[-1] filename = path.split(location)[-1]
async with open_async(location, mode='rb') as _file: async with open_async(location, mode='rb') as _file:

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'])
@ -84,11 +85,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)
uri = host + uri if isinstance(host, str):
uri = host + uri
if uri in self.routes_all: else:
raise RouteExists("Route already registered: {}".format(uri)) for h in host:
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:
@ -122,9 +127,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']:

View File

@ -237,7 +237,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)
@ -246,9 +246,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, sock=None, after_start=None, before_stop=None, after_stop=None, ssl=None,
workers=1, loop=None, protocol=HttpProtocol, backlog=100, sock=None, workers=1, loop=None, protocol=HttpProtocol,
stop_event=None, logger=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.
@ -264,7 +265,7 @@ class Sanic:
received before it is respected received before it is respected
:param after_stop: Functions to be executed when all requests are :param after_stop: Functions to be executed when all requests are
complete complete
:param ssl: SSLContext for SSL encryption of worker(s)
:param sock: Socket for the server to accept connections from :param sock: Socket for the server to accept connections from
:param workers: Number of processes :param workers: Number of processes
received before it is respected received before it is respected
@ -285,6 +286,7 @@ class Sanic:
'host': host, 'host': host,
'port': port, 'port': port,
'sock': sock, 'sock': sock,
'ssl': ssl,
'debug': debug, 'debug': debug,
'request_handler': self.handle_request, 'request_handler': self.handle_request,
'error_handler': self.error_handler, 'error_handler': self.error_handler,
@ -322,7 +324,11 @@ class Sanic:
log.debug(self.config.LOGO) log.debug(self.config.LOGO)
# Serve # Serve
log.info('Goin\' Fast @ http://{}:{}'.format(host, port)) if ssl is None:
proto = "http"
else:
proto = "https"
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
try: try:
if workers == 1: if workers == 1:

View File

@ -248,7 +248,7 @@ def trigger_events(events, loop):
def serve(host, port, request_handler, error_handler, before_start=None, def serve(host, port, request_handler, error_handler, before_start=None,
after_start=None, before_stop=None, after_stop=None, debug=False, after_start=None, before_stop=None, after_stop=None, debug=False,
request_timeout=60, sock=None, request_max_size=None, request_timeout=60, ssl=None, sock=None, request_max_size=None,
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100): reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100):
""" """
Starts asynchronous HTTP Server on an individual process. Starts asynchronous HTTP Server on an individual process.
@ -269,6 +269,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
argument `loop` argument `loop`
:param debug: Enables debug output (slows server) :param debug: Enables debug output (slows server)
:param request_timeout: time in seconds :param request_timeout: time in seconds
:param ssl: SSLContext
:param sock: Socket for the server to accept connections from :param sock: Socket for the server to accept connections from
:param request_max_size: size in bytes, `None` for no limit :param request_max_size: size in bytes, `None` for no limit
:param reuse_port: `True` for multiple workers :param reuse_port: `True` for multiple workers
@ -301,6 +302,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
server, server,
host, host,
port, port,
ssl=ssl,
reuse_port=reuse_port, reuse_port=reuse_port,
sock=sock, sock=sock,
backlog=backlog backlog=backlog

View File

@ -65,3 +65,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!"