Merge branch 'master' of git://github.com/channelcat/sanic

This commit is contained in:
Jack Fischer 2016-12-03 15:08:07 -05:00
commit c34427690a
17 changed files with 556 additions and 23 deletions

View File

@ -52,6 +52,7 @@ app.run(host="0.0.0.0", port=8000)
* [Middleware](docs/middleware.md) * [Middleware](docs/middleware.md)
* [Exceptions](docs/exceptions.md) * [Exceptions](docs/exceptions.md)
* [Blueprints](docs/blueprints.md) * [Blueprints](docs/blueprints.md)
* [Class Based Views](docs/class_based_views.md)
* [Cookies](docs/cookies.md) * [Cookies](docs/cookies.md)
* [Static Files](docs/static_files.md) * [Static Files](docs/static_files.md)
* [Deploying](docs/deploying.md) * [Deploying](docs/deploying.md)

44
docs/class_based_views.md Normal file
View File

@ -0,0 +1,44 @@
# Class based views
Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response.
## Examples
```python
from sanic import Sanic
from sanic.views import HTTPMethodView
app = Sanic('some_name')
class SimpleView(HTTPMethodView):
def get(self, request):
return text('I am get method')
def post(self, request):
return text('I am post method')
def put(self, request):
return text('I am put method')
def patch(self, request):
return text('I am patch method')
def delete(self, request):
return text('I am delete method')
app.add_route(SimpleView(), '/')
```
If you need any url params just mention them in method definition:
```python
class NameView(HTTPMethodView):
def get(self, request, name):
return text('Hello {}'.format(name))
app.add_route(NameView(), '/<name')
```

View File

@ -29,4 +29,16 @@ async def person_handler(request, name):
async def folder_handler(request, folder_id): async def folder_handler(request, folder_id):
return text('Folder - {}'.format(folder_id)) return text('Folder - {}'.format(folder_id))
async def handler1(request):
return text('OK')
app.add_route(handler1, '/test')
async def handler(request, name):
return text('Folder - {}'.format(name))
app.add_route(handler, '/folder/<name>')
async def person_handler(request, name):
return text('Person - {}'.format(name))
app.add_route(handler, '/person/<name:[A-z]>')
``` ```

View File

@ -0,0 +1,21 @@
from sanic import Sanic
import asyncio
from sanic.response import text
from sanic.config import Config
from sanic.exceptions import RequestTimeout
Config.REQUEST_TIMEOUT = 1
app = Sanic(__name__)
@app.route('/')
async def test(request):
await asyncio.sleep(3)
return text('Hello, world!')
@app.exception(RequestTimeout)
def timeout(request, exception):
return text('RequestTimeout from error_handler.', 408)
app.run(host='0.0.0.0', port=8000)

View File

@ -11,3 +11,4 @@ bottle
kyoukai kyoukai
falcon falcon
tornado tornado
aiofiles

View File

@ -1,6 +1,6 @@
from .sanic import Sanic from .sanic import Sanic
from .blueprints import Blueprint from .blueprints import Blueprint
__version__ = '0.1.7' __version__ = '0.1.8'
__all__ = ['Sanic', 'Blueprint'] __all__ = ['Sanic', 'Blueprint']

View File

@ -91,6 +91,12 @@ class Blueprint:
return handler return handler
return decorator return decorator
def add_route(self, handler, uri, methods=None):
"""
"""
self.record(lambda s: s.add_route(handler, uri, methods))
return handler
def listener(self, event): def listener(self, event):
""" """
""" """

View File

@ -30,6 +30,10 @@ class FileNotFound(NotFound):
self.relative_url = relative_url self.relative_url = relative_url
class RequestTimeout(SanicException):
status_code = 408
class Handler: class Handler:
handlers = None handlers = None

View File

@ -67,7 +67,7 @@ class Request(dict):
try: try:
self.parsed_json = json_loads(self.body) self.parsed_json = json_loads(self.body)
except Exception: except Exception:
log.exception("failed when parsing body as json") log.exception("Failed when parsing body as json")
return self.parsed_json return self.parsed_json
@ -89,7 +89,7 @@ class Request(dict):
self.parsed_form, self.parsed_files = ( self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary)) parse_multipart_form(self.body, boundary))
except Exception: except Exception:
log.exception("failed when parsing form") log.exception("Failed when parsing form")
return self.parsed_form return self.parsed_form

View File

@ -30,11 +30,17 @@ class Router:
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...]) @sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...])
def my_route(request, my_parameter): def my_route(request, my_parameter):
do stuff... do stuff...
or
@sanic.route('/my/url/<my_paramter>:type', methods['GET', 'POST', ...])
def my_route_with_type(request, my_parameter):
do stuff...
Parameters will be passed as keyword arguments to the request handling Parameters will be passed as keyword arguments to the request handling
function provided Parameters can also have a type by appending :type to function. Provided parameters can also have a type by appending :type to
the <parameter>. If no type is provided, a string is expected. A regular the <parameter>. Given parameter must be able to be type-casted to this.
expression can also be passed in as the type If no type is provided, a string is expected. A regular expression can
also be passed in as the type. The argument given to the function will
always be a string, independent of the type.
""" """
routes_static = None routes_static = None
routes_dynamic = None routes_dynamic = None

View File

@ -60,6 +60,19 @@ class Sanic:
return response return response
def add_route(self, handler, uri, methods=None):
"""
A helper method to register class instance or
functions as a handler to the application url
routes.
:param handler: function or class instance
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:return: function or class instance
"""
self.route(uri=uri, methods=methods)(handler)
return handler
# Decorator # Decorator
def exception(self, *exceptions): def exception(self, *exceptions):
""" """
@ -250,6 +263,7 @@ class Sanic:
'sock': sock, 'sock': sock,
'debug': debug, 'debug': debug,
'request_handler': self.handle_request, 'request_handler': self.handle_request,
'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT, 'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE, 'request_max_size': self.config.REQUEST_MAX_SIZE,
'loop': loop 'loop': loop

View File

@ -4,7 +4,8 @@ from inspect import isawaitable
from multidict import CIMultiDict from multidict import CIMultiDict
from signal import SIGINT, SIGTERM from signal import SIGINT, SIGTERM
from time import time from time import time
import httptools from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError
try: try:
import uvloop as async_loop import uvloop as async_loop
@ -13,6 +14,7 @@ except ImportError:
from .log import log from .log import log
from .request import Request from .request import Request
from .exceptions import RequestTimeout
class Signal: class Signal:
@ -33,8 +35,8 @@ class HttpProtocol(asyncio.Protocol):
# connection management # connection management
'_total_request_size', '_timeout_handler', '_last_communication_time') '_total_request_size', '_timeout_handler', '_last_communication_time')
def __init__(self, *, loop, request_handler, signal=Signal(), def __init__(self, *, loop, request_handler, error_handler,
connections={}, request_timeout=60, signal=Signal(), connections={}, request_timeout=60,
request_max_size=None): request_max_size=None):
self.loop = loop self.loop = loop
self.transport = None self.transport = None
@ -45,11 +47,13 @@ class HttpProtocol(asyncio.Protocol):
self.signal = signal self.signal = signal
self.connections = connections self.connections = connections
self.request_handler = request_handler self.request_handler = request_handler
self.error_handler = error_handler
self.request_timeout = request_timeout self.request_timeout = request_timeout
self.request_max_size = request_max_size self.request_max_size = request_max_size
self._total_request_size = 0 self._total_request_size = 0
self._timeout_handler = None self._timeout_handler = None
self._last_request_time = None self._last_request_time = None
self._request_handler_task = None
# -------------------------------------------- # # -------------------------------------------- #
# Connection # Connection
@ -75,7 +79,11 @@ class HttpProtocol(asyncio.Protocol):
self._timeout_handler = \ self._timeout_handler = \
self.loop.call_later(time_left, self.connection_timeout) self.loop.call_later(time_left, self.connection_timeout)
else: else:
self.bail_out("Request timed out, connection closed") if self._request_handler_task:
self._request_handler_task.cancel()
response = self.error_handler.response(
self.request, RequestTimeout('Request Timeout'))
self.write_response(response)
# -------------------------------------------- # # -------------------------------------------- #
# Parsing # Parsing
@ -94,12 +102,12 @@ class HttpProtocol(asyncio.Protocol):
if self.parser is None: if self.parser is None:
assert self.request is None assert self.request is None
self.headers = [] self.headers = []
self.parser = httptools.HttpRequestParser(self) self.parser = HttpRequestParser(self)
# Parse request chunk or close connection # Parse request chunk or close connection
try: try:
self.parser.feed_data(data) self.parser.feed_data(data)
except httptools.parser.errors.HttpParserError as e: except HttpParserError as e:
self.bail_out( self.bail_out(
"Invalid request data, connection closed ({})".format(e)) "Invalid request data, connection closed ({})".format(e))
@ -132,7 +140,7 @@ class HttpProtocol(asyncio.Protocol):
self.request.body = body self.request.body = body
def on_message_complete(self): def on_message_complete(self):
self.loop.create_task( self._request_handler_task = self.loop.create_task(
self.request_handler(self.request, self.write_response)) self.request_handler(self.request, self.write_response))
# -------------------------------------------- # # -------------------------------------------- #
@ -165,6 +173,7 @@ class HttpProtocol(asyncio.Protocol):
self.request = None self.request = None
self.url = None self.url = None
self.headers = None self.headers = None
self._request_handler_task = None
self._total_request_size = 0 self._total_request_size = 0
def close_if_idle(self): def close_if_idle(self):
@ -204,8 +213,8 @@ def trigger_events(events, loop):
loop.run_until_complete(result) loop.run_until_complete(result)
def serve(host, port, request_handler, before_start=None, after_start=None, def serve(host, port, request_handler, error_handler, before_start=None,
before_stop=None, after_stop=None, after_start=None, before_stop=None, after_stop=None,
debug=False, request_timeout=60, sock=None, debug=False, request_timeout=60, sock=None,
request_max_size=None, reuse_port=False, loop=None): request_max_size=None, reuse_port=False, loop=None):
""" """
@ -240,6 +249,7 @@ def serve(host, port, request_handler, before_start=None, after_start=None,
connections=connections, connections=connections,
signal=signal, signal=signal,
request_handler=request_handler, request_handler=request_handler,
error_handler=error_handler,
request_timeout=request_timeout, request_timeout=request_timeout,
request_max_size=request_max_size, request_max_size=request_max_size,
), host, port, reuse_port=reuse_port, sock=sock) ), host, port, reuse_port=reuse_port, sock=sock)

View File

@ -16,7 +16,8 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
loop=None, *request_args, **request_kwargs): loop=None, debug=False, *request_args,
**request_kwargs):
results = [] results = []
exceptions = [] exceptions = []
@ -34,7 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
exceptions.append(e) exceptions.append(e)
app.stop() app.stop()
app.run(host=HOST, port=42101, after_start=_collect_response, loop=loop) app.run(host=HOST, debug=debug, port=42101,
after_start=_collect_response, loop=loop)
if exceptions: if exceptions:
raise ValueError("Exception during request: {}".format(exceptions)) raise ValueError("Exception during request: {}".format(exceptions))
@ -45,11 +47,11 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
return request, response return request, response
except: except:
raise ValueError( raise ValueError(
"request and response object expected, got ({})".format( "Request and response object expected, got ({})".format(
results)) results))
else: else:
try: try:
return results[0] return results[0]
except: except:
raise ValueError( raise ValueError(
"request object expected, got ({})".format(results)) "Request object expected, got ({})".format(results))

39
sanic/views.py Normal file
View File

@ -0,0 +1,39 @@
from .exceptions import InvalidUsage
class HTTPMethodView:
""" Simple class based implementation of view for the sanic.
You should implement methods (get, post, put, patch, delete) for the class
to every HTTP method you want to support.
For example:
class DummyView(View):
def get(self, request, *args, **kwargs):
return text('I am get method')
def put(self, request, *args, **kwargs):
return text('I am put method')
etc.
If someone tries to use a non-implemented method, there will be a
405 response.
If you need any url params just mention them in method definition:
class DummyView(View):
def get(self, request, my_param_here, *args, **kwargs):
return text('I am get method with %s' % my_param_here)
To add the view into the routing you could use
1) app.add_route(DummyView(), '/')
2) app.route('/')(DummyView())
"""
def __call__(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None)
if handler:
return handler(request, *args, **kwargs)
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)

View File

@ -0,0 +1,40 @@
from sanic import Sanic
import asyncio
from sanic.response import text
from sanic.exceptions import RequestTimeout
from sanic.utils import sanic_endpoint_test
from sanic.config import Config
Config.REQUEST_TIMEOUT = 1
request_timeout_app = Sanic('test_request_timeout')
request_timeout_default_app = Sanic('test_request_timeout_default')
@request_timeout_app.route('/1')
async def handler_1(request):
await asyncio.sleep(1)
return text('OK')
@request_timeout_app.exception(RequestTimeout)
def handler_exception(request, exception):
return text('Request Timeout from error_handler.', 408)
def test_server_error_request_timeout():
request, response = sanic_endpoint_test(request_timeout_app, uri='/1')
assert response.status == 408
assert response.text == 'Request Timeout from error_handler.'
@request_timeout_default_app.route('/1')
async def handler_2(request):
await asyncio.sleep(1)
return text('OK')
def test_default_server_error_request_timeout():
request, response = sanic_endpoint_test(
request_timeout_default_app, uri='/1')
assert response.status == 408
assert response.text == 'Error: Request Timeout'

View File

@ -84,7 +84,7 @@ def test_dynamic_route_int():
def test_dynamic_route_number(): def test_dynamic_route_number():
app = Sanic('test_dynamic_route_int') app = Sanic('test_dynamic_route_number')
results = [] results = []
@ -105,7 +105,7 @@ def test_dynamic_route_number():
def test_dynamic_route_regex(): def test_dynamic_route_regex():
app = Sanic('test_dynamic_route_int') app = Sanic('test_dynamic_route_regex')
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>') @app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>')
async def handler(request, folder_id): async def handler(request, folder_id):
@ -145,7 +145,7 @@ def test_dynamic_route_unhashable():
def test_route_duplicate(): def test_route_duplicate():
app = Sanic('test_dynamic_route') app = Sanic('test_route_duplicate')
with pytest.raises(RouteExists): with pytest.raises(RouteExists):
@app.route('/test') @app.route('/test')
@ -178,3 +178,181 @@ def test_method_not_allowed():
request, response = sanic_endpoint_test(app, method='post', uri='/test') request, response = sanic_endpoint_test(app, method='post', uri='/test')
assert response.status == 405 assert response.status == 405
def test_static_add_route():
app = Sanic('test_static_add_route')
async def handler1(request):
return text('OK1')
async def handler2(request):
return text('OK2')
app.add_route(handler1, '/test')
app.add_route(handler2, '/test2')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, uri='/test2')
assert response.text == 'OK2'
def test_dynamic_add_route():
app = Sanic('test_dynamic_add_route')
results = []
async def handler(request, name):
results.append(name)
return text('OK')
app.add_route(handler, '/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.text == 'OK'
assert results[0] == 'test123'
def test_dynamic_add_route_string():
app = Sanic('test_dynamic_add_route_string')
results = []
async def handler(request, name):
results.append(name)
return text('OK')
app.add_route(handler, '/folder/<name:string>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.text == 'OK'
assert results[0] == 'test123'
request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico')
assert response.text == 'OK'
assert results[1] == 'favicon.ico'
def test_dynamic_add_route_int():
app = Sanic('test_dynamic_add_route_int')
results = []
async def handler(request, folder_id):
results.append(folder_id)
return text('OK')
app.add_route(handler, '/folder/<folder_id:int>')
request, response = sanic_endpoint_test(app, uri='/folder/12345')
assert response.text == 'OK'
assert type(results[0]) is int
request, response = sanic_endpoint_test(app, uri='/folder/asdf')
assert response.status == 404
def test_dynamic_add_route_number():
app = Sanic('test_dynamic_add_route_number')
results = []
async def handler(request, weight):
results.append(weight)
return text('OK')
app.add_route(handler, '/weight/<weight:number>')
request, response = sanic_endpoint_test(app, uri='/weight/12345')
assert response.text == 'OK'
assert type(results[0]) is float
request, response = sanic_endpoint_test(app, uri='/weight/1234.56')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/weight/1234-56')
assert response.status == 404
def test_dynamic_add_route_regex():
app = Sanic('test_dynamic_route_int')
async def handler(request, folder_id):
return text('OK')
app.add_route(handler, '/folder/<folder_id:[A-Za-z0-9]{0,4}>')
request, response = sanic_endpoint_test(app, uri='/folder/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test1')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test-123')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/')
assert response.status == 200
def test_dynamic_add_route_unhashable():
app = Sanic('test_dynamic_add_route_unhashable')
async def handler(request, unhashable):
return text('OK')
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/')
assert response.status == 404
def test_add_route_duplicate():
app = Sanic('test_add_route_duplicate')
with pytest.raises(RouteExists):
async def handler1(request):
pass
async def handler2(request):
pass
app.add_route(handler1, '/test')
app.add_route(handler2, '/test')
with pytest.raises(RouteExists):
async def handler1(request, dynamic):
pass
async def handler2(request, dynamic):
pass
app.add_route(handler1, '/test/<dynamic>/')
app.add_route(handler2, '/test/<dynamic>/')
def test_add_route_method_not_allowed():
app = Sanic('test_add_route_method_not_allowed')
async def handler(request):
return text('OK')
app.add_route(handler, '/test', methods=['GET'])
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, method='post', uri='/test')
assert response.status == 405

155
tests/test_views.py Normal file
View File

@ -0,0 +1,155 @@
from sanic import Sanic
from sanic.response import text, HTTPResponse
from sanic.views import HTTPMethodView
from sanic.blueprints import Blueprint
from sanic.request import Request
from sanic.utils import sanic_endpoint_test
def test_methods():
app = Sanic('test_methods')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
def post(self, request):
return text('I am post method')
def put(self, request):
return text('I am put method')
def patch(self, request):
return text('I am patch method')
def delete(self, request):
return text('I am delete method')
app.add_route(DummyView(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
request, response = sanic_endpoint_test(app, method="post")
assert response.text == 'I am post method'
request, response = sanic_endpoint_test(app, method="put")
assert response.text == 'I am put method'
request, response = sanic_endpoint_test(app, method="patch")
assert response.text == 'I am patch method'
request, response = sanic_endpoint_test(app, method="delete")
assert response.text == 'I am delete method'
def test_unexisting_methods():
app = Sanic('test_unexisting_methods')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
app.add_route(DummyView(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
request, response = sanic_endpoint_test(app, method="post")
assert response.text == 'Error: Method POST not allowed for URL /'
def test_argument_methods():
app = Sanic('test_argument_methods')
class DummyView(HTTPMethodView):
def get(self, request, my_param_here):
return text('I am get method with %s' % my_param_here)
app.add_route(DummyView(), '/<my_param_here>')
request, response = sanic_endpoint_test(app, uri='/test123')
assert response.text == 'I am get method with test123'
def test_with_bp():
app = Sanic('test_with_bp')
bp = Blueprint('test_text')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
bp.add_route(DummyView(), '/')
app.blueprint(bp)
request, response = sanic_endpoint_test(app)
assert response.text == 'I am get method'
def test_with_bp_with_url_prefix():
app = Sanic('test_with_bp_with_url_prefix')
bp = Blueprint('test_text', url_prefix='/test1')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
bp.add_route(DummyView(), '/')
app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/')
assert response.text == 'I am get method'
def test_with_middleware():
app = Sanic('test_with_middleware')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
app.add_route(DummyView(), '/')
results = []
@app.middleware
async def handler(request):
results.append(request)
request, response = sanic_endpoint_test(app)
assert response.text == 'I am get method'
assert type(results[0]) is Request
def test_with_middleware_response():
app = Sanic('test_with_middleware_response')
results = []
@app.middleware('request')
async def process_response(request):
results.append(request)
@app.middleware('response')
async def process_response(request, response):
results.append(request)
results.append(response)
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
app.add_route(DummyView(), '/')
request, response = sanic_endpoint_test(app)
assert response.text == 'I am get method'
assert type(results[0]) is Request
assert type(results[1]) is Request
assert issubclass(type(results[2]), HTTPResponse)