Merge pull request #167 from AntonDnepr/class-based-views
Class based views
This commit is contained in:
commit
cce47a633a
|
@ -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
44
docs/class_based_views.md
Normal 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')
|
||||||
|
|
||||||
|
```
|
|
@ -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]>')
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -11,3 +11,4 @@ bottle
|
||||||
kyoukai
|
kyoukai
|
||||||
falcon
|
falcon
|
||||||
tornado
|
tornado
|
||||||
|
aiofiles
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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))
|
||||||
|
|
36
sanic/views.py
Normal file
36
sanic/views.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
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 try use not implemented method, there will be 405 response
|
||||||
|
|
||||||
|
If you need any url params just mention them in method definition like:
|
||||||
|
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)
|
|
@ -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
155
tests/test_views.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user