diff --git a/docs/class_based_views.md b/docs/class_based_views.md new file mode 100644 index 00000000..b5f8ee02 --- /dev/null +++ b/docs/class_based_views.md @@ -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 try to use not implemented method, there will be 405 response. + +## Examples +```python +from sanic import Sanic +from sanic.views import MethodView + +app = Sanic('some_name') + + +class SimpleView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + def post(self, request, *args, **kwargs): + return text('I am post method') + + def put(self, request, *args, **kwargs): + return text('I am put method') + + def patch(self, request, *args, **kwargs): + return text('I am patch method') + + def delete(self, request, *args, **kwargs): + 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(MethodView): + + def get(self, request, name, *args, **kwargs): + return text('Hello {}'.format(name)) + +app.add_route(NameView(), '/') + +async def person_handler(request, name): + return text('Person - {}'.format(name)) +app.add_route(handler, '/person/') + ``` diff --git a/requirements-dev.txt b/requirements-dev.txt index 00feb17d..1c34d695 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,3 +11,4 @@ bottle kyoukai falcon tornado +aiofiles diff --git a/sanic/blueprints.py b/sanic/blueprints.py index bfef8557..92e376f1 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -91,6 +91,12 @@ class Blueprint: return handler 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): """ """ diff --git a/sanic/sanic.py b/sanic/sanic.py index af284c00..7a9c35ad 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -60,6 +60,17 @@ class Sanic: 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 def exception(self, *exceptions): """ diff --git a/sanic/utils.py b/sanic/utils.py index 0749464b..8190c1d0 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -16,7 +16,7 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): 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 = [] exceptions = [] @@ -34,7 +34,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, exceptions.append(e) 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: raise ValueError("Exception during request: {}".format(exceptions)) diff --git a/sanic/views.py b/sanic/views.py new file mode 100644 index 00000000..9cb04247 --- /dev/null +++ b/sanic/views.py @@ -0,0 +1,33 @@ +from .exceptions import InvalidUsage + + +class MethodView: + """ 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) diff --git a/tests/test_routes.py b/tests/test_routes.py index 8b0fd9f6..38591e53 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -84,7 +84,7 @@ def test_dynamic_route_int(): def test_dynamic_route_number(): - app = Sanic('test_dynamic_route_int') + app = Sanic('test_dynamic_route_number') results = [] @@ -105,7 +105,7 @@ def test_dynamic_route_number(): def test_dynamic_route_regex(): - app = Sanic('test_dynamic_route_int') + app = Sanic('test_dynamic_route_regex') @app.route('/folder/') async def handler(request, folder_id): @@ -145,7 +145,7 @@ def test_dynamic_route_unhashable(): def test_route_duplicate(): - app = Sanic('test_dynamic_route') + app = Sanic('test_route_duplicate') with pytest.raises(RouteExists): @app.route('/test') @@ -178,3 +178,181 @@ def test_method_not_allowed(): request, response = sanic_endpoint_test(app, method='post', uri='/test') 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/') + 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/') + 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/') + + 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/') + + 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/') + + 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//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//') + app.add_route(handler2, '/test//') + + +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 diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 00000000..251b7a10 --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,155 @@ +from sanic import Sanic +from sanic.response import text, HTTPResponse +from sanic.views import MethodView +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(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + def post(self, request, *args, **kwargs): + return text('I am post method') + + def put(self, request, *args, **kwargs): + return text('I am put method') + + def patch(self, request, *args, **kwargs): + return text('I am patch method') + + def delete(self, request, *args, **kwargs): + 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(MethodView): + + def get(self, request, *args, **kwargs): + 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(MethodView): + + def get(self, request, my_param_here, *args, **kwargs): + return text('I am get method with %s' % my_param_here) + + app.add_route(DummyView(), '/') + + 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(MethodView): + + def get(self, request, *args, **kwargs): + 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(MethodView): + + def get(self, request, *args, **kwargs): + 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(MethodView): + + def get(self, request, *args, **kwargs): + 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(MethodView): + + def get(self, request, *args, **kwargs): + 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)