From 947364e15f72aa61b76c6c1246ba1891b8f84417 Mon Sep 17 00:00:00 2001 From: jiaxiaolei Date: Sun, 20 Aug 2017 11:11:14 +0800 Subject: [PATCH 01/16] feat(exapmles): add `add_task_sanic.py` --- examples/add_task_sanic.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/add_task_sanic.py diff --git a/examples/add_task_sanic.py b/examples/add_task_sanic.py new file mode 100644 index 00000000..52b4e6bb --- /dev/null +++ b/examples/add_task_sanic.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +import asyncio + +from sanic import Sanic + +app = Sanic() + + +async def notify_server_started_after_five_seconds(): + await asyncio.sleep(5) + print('Server successfully started!') + +app.add_task(notify_server_started_after_five_seconds()) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) From 439ff11d1343b37a59915b2a42c570b6fca187f6 Mon Sep 17 00:00:00 2001 From: Igor Hatarist Date: Sun, 20 Aug 2017 19:28:09 +0300 Subject: [PATCH 02/16] Added a line on headers in the "Request Data" docs --- docs/sanic/request_data.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sanic/request_data.md b/docs/sanic/request_data.md index bf5ae4a8..a62b7e6a 100644 --- a/docs/sanic/request_data.md +++ b/docs/sanic/request_data.md @@ -71,6 +71,8 @@ The following variables are accessible as properties on `Request` objects: return text("You are trying to create a user with the following POST: %s" % request.body) ``` +- `headers` (dict) - A case-insensitive dictionary that contains the request headers. + - `ip` (str) - IP address of the requester. - `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object. From ef81a9f54703aa0adaa8132cc54855353c9a6e33 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 20 Aug 2017 23:11:38 -0700 Subject: [PATCH 03/16] make strict_slashes default value configurable --- sanic/app.py | 37 +++++++++++++++++++++++++------------ sanic/blueprints.py | 36 +++++++++++++++++++++++++----------- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index d4ee8275..53a6a8f6 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -28,7 +28,7 @@ class Sanic: def __init__(self, name=None, router=None, error_handler=None, load_env=True, request_class=None, - log_config=LOGGING): + log_config=LOGGING, strict_slashes=False): if log_config: logging.config.dictConfig(log_config) # Only set up a default log handler if the @@ -58,6 +58,7 @@ class Sanic: self._blueprint_order = [] self.debug = None self.sock = None + self.strict_slashes = strict_slashes self.listeners = defaultdict(list) self.is_running = False self.is_request_stream = False @@ -111,7 +112,7 @@ class Sanic: # Decorator def route(self, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=False, stream=False, version=None): + strict_slashes=None, stream=False, version=None): """Decorate a function to be registered as a route :param uri: path of the URL @@ -130,6 +131,9 @@ class Sanic: if stream: self.is_request_stream = True + if strict_slashes is None: + strict_slashes = self.strict_slashes + def response(handler): if stream: handler.is_stream = stream @@ -141,42 +145,42 @@ class Sanic: return response # Shorthand method decorators - def get(self, uri, host=None, strict_slashes=False, version=None): + def get(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=frozenset({"GET"}), host=host, strict_slashes=strict_slashes, version=version) - def post(self, uri, host=None, strict_slashes=False, stream=False, + def post(self, uri, host=None, strict_slashes=None, stream=False, version=None): return self.route(uri, methods=frozenset({"POST"}), host=host, strict_slashes=strict_slashes, stream=stream, version=version) - def put(self, uri, host=None, strict_slashes=False, stream=False, + def put(self, uri, host=None, strict_slashes=None, stream=False, version=None): return self.route(uri, methods=frozenset({"PUT"}), host=host, strict_slashes=strict_slashes, stream=stream, version=version) - def head(self, uri, host=None, strict_slashes=False, version=None): + def head(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=frozenset({"HEAD"}), host=host, strict_slashes=strict_slashes, version=version) - def options(self, uri, host=None, strict_slashes=False, version=None): + def options(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=frozenset({"OPTIONS"}), host=host, strict_slashes=strict_slashes, version=version) - def patch(self, uri, host=None, strict_slashes=False, stream=False, + def patch(self, uri, host=None, strict_slashes=None, stream=False, version=None): return self.route(uri, methods=frozenset({"PATCH"}), host=host, strict_slashes=strict_slashes, stream=stream, version=version) - def delete(self, uri, host=None, strict_slashes=False, version=None): + def delete(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=frozenset({"DELETE"}), host=host, strict_slashes=strict_slashes, version=version) def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=False, version=None): + strict_slashes=None, version=None): """A helper method to register class instance or functions as a handler to the application url routes. @@ -208,13 +212,16 @@ class Sanic: stream = True break + if strict_slashes is None: + strict_slashes = self.strict_slashes + self.route(uri=uri, methods=methods, host=host, strict_slashes=strict_slashes, stream=stream, version=version)(handler) return handler # Decorator - def websocket(self, uri, host=None, strict_slashes=False, + def websocket(self, uri, host=None, strict_slashes=None, subprotocols=None): """Decorate a function to be registered as a websocket route :param uri: path of the URL @@ -230,6 +237,9 @@ class Sanic: if not uri.startswith('/'): uri = '/' + uri + if strict_slashes is None: + strict_slashes = self.strict_slashes + def response(handler): async def websocket_handler(request, *args, **kwargs): request.app = self @@ -261,8 +271,11 @@ class Sanic: return response def add_websocket_route(self, handler, uri, host=None, - strict_slashes=False): + strict_slashes=None): """A helper method to register a function as a websocket route.""" + if strict_slashes is None: + strict_slashes = self.strict_slashes + return self.websocket(uri, host=host, strict_slashes=strict_slashes)(handler) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 0e97903b..b899481b 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -14,7 +14,11 @@ FutureStatic = namedtuple('Route', class Blueprint: - def __init__(self, name, url_prefix=None, host=None, version=None): + + def __init__(self, name, + url_prefix=None, + host=None, version=None, + strict_slashes=False): """Create a new blueprint :param name: unique name of the blueprint @@ -31,6 +35,7 @@ class Blueprint: self.middlewares = [] self.statics = [] self.version = version + self.strict_slashes = strict_slashes def register(self, app, options): """Register the blueprint to the sanic app.""" @@ -94,12 +99,15 @@ class Blueprint: app.listener(event)(listener) def route(self, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=False, stream=False, version=None): + strict_slashes=None, stream=False, version=None): """Create a blueprint route from a decorated function. :param uri: endpoint at which the route will be accessible. :param methods: list of acceptable HTTP methods. """ + if strict_slashes is None: + strict_slashes = self.strict_slashes + def decorator(handler): route = FutureRoute( handler, uri, methods, host, strict_slashes, stream, version) @@ -108,7 +116,7 @@ class Blueprint: return decorator def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=False, version=None): + strict_slashes=None, version=None): """Create a blueprint route from a function. :param handler: function for handling uri requests. Accepts function, @@ -125,6 +133,9 @@ class Blueprint: if getattr(handler.view_class, method.lower(), None): methods.add(method) + if strict_slashes is None: + strict_slashes = self.strict_slashes + # handle composition view differently if isinstance(handler, CompositionView): methods = handler.handlers.keys() @@ -133,11 +144,14 @@ class Blueprint: strict_slashes=strict_slashes, version=version)(handler) return handler - def websocket(self, uri, host=None, strict_slashes=False, version=None): + def websocket(self, uri, host=None, strict_slashes=None, version=None): """Create a blueprint websocket route from a decorated function. :param uri: endpoint at which the route will be accessible. """ + if strict_slashes is None: + strict_slashes = self.strict_slashes + def decorator(handler): route = FutureRoute(handler, uri, [], host, strict_slashes, False, version) @@ -199,36 +213,36 @@ class Blueprint: self.statics.append(static) # Shorthand method decorators - def get(self, uri, host=None, strict_slashes=False, version=None): + def get(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=["GET"], host=host, strict_slashes=strict_slashes, version=version) - def post(self, uri, host=None, strict_slashes=False, stream=False, + def post(self, uri, host=None, strict_slashes=None, stream=False, version=None): return self.route(uri, methods=["POST"], host=host, strict_slashes=strict_slashes, stream=stream, version=version) - def put(self, uri, host=None, strict_slashes=False, stream=False, + def put(self, uri, host=None, strict_slashes=None, stream=False, version=None): return self.route(uri, methods=["PUT"], host=host, strict_slashes=strict_slashes, stream=stream, version=version) - def head(self, uri, host=None, strict_slashes=False, version=None): + def head(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=["HEAD"], host=host, strict_slashes=strict_slashes, version=version) - def options(self, uri, host=None, strict_slashes=False, version=None): + def options(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=["OPTIONS"], host=host, strict_slashes=strict_slashes, version=version) - def patch(self, uri, host=None, strict_slashes=False, stream=False, + def patch(self, uri, host=None, strict_slashes=None, stream=False, version=None): return self.route(uri, methods=["PATCH"], host=host, strict_slashes=strict_slashes, stream=stream, version=version) - def delete(self, uri, host=None, strict_slashes=False, version=None): + def delete(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=["DELETE"], host=host, strict_slashes=strict_slashes, version=version) From 5d23c7644b5003ad2cda6cae8fb42729db4e6628 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 20 Aug 2017 23:37:22 -0700 Subject: [PATCH 04/16] add unit tests --- tests/test_blueprints.py | 59 ++++++++++++++++++++++++++++++++++++++++ tests/test_routes.py | 30 ++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 5cb356c2..7e713da6 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -78,6 +78,65 @@ def test_bp_strict_slash(): request, response = app.test_client.post('/post') assert response.status == 404 +def test_bp_strict_slash_default_value(): + app = Sanic('test_route_strict_slash') + bp = Blueprint('test_text', strict_slashes=True) + + @bp.get('/get') + def handler(request): + return text('OK') + + @bp.post('/post/') + def handler(request): + return text('OK') + + app.blueprint(bp) + + request, response = app.test_client.get('/get/') + assert response.status == 404 + + request, response = app.test_client.post('/post') + assert response.status == 404 + +def test_bp_strict_slash_without_passing_default_value(): + app = Sanic('test_route_strict_slash') + bp = Blueprint('test_text') + + @bp.get('/get') + def handler(request): + return text('OK') + + @bp.post('/post/') + def handler(request): + return text('OK') + + app.blueprint(bp) + + request, response = app.test_client.get('/get/') + assert response.text == 'OK' + + request, response = app.test_client.post('/post') + assert response.text == 'OK' + +def test_bp_strict_slash_default_value_can_be_overwritten(): + app = Sanic('test_route_strict_slash') + bp = Blueprint('test_text', strict_slashes=True) + + @bp.get('/get', strict_slashes=False) + def handler(request): + return text('OK') + + @bp.post('/post/', strict_slashes=False) + def handler(request): + return text('OK') + + app.blueprint(bp) + + request, response = app.test_client.get('/get/') + assert response.text == 'OK' + + request, response = app.test_client.post('/post') + assert response.text == 'OK' def test_bp_with_url_prefix(): app = Sanic('test_text') diff --git a/tests/test_routes.py b/tests/test_routes.py index b356c2d5..b7228d29 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -71,6 +71,36 @@ def test_route_strict_slash(): request, response = app.test_client.post('/post') assert response.status == 404 +def test_route_strict_slash_default_value(): + app = Sanic('test_route_strict_slash', strict_slashes=True) + + @app.get('/get') + def handler(request): + return text('OK') + + request, response = app.test_client.get('/get/') + assert response.status == 404 + +def test_route_strict_slash_without_passing_default_value(): + app = Sanic('test_route_strict_slash') + + @app.get('/get') + def handler(request): + return text('OK') + + request, response = app.test_client.get('/get/') + assert response.text == 'OK' + +def test_route_strict_slash_default_value_can_be_overwritten(): + app = Sanic('test_route_strict_slash', strict_slashes=True) + + @app.get('/get', strict_slashes=False) + def handler(request): + return text('OK') + + request, response = app.test_client.get('/get/') + assert response.text == 'OK' + def test_route_optional_slash(): app = Sanic('test_route_optional_slash') From 63babae63db0f66ac0941f639c41fc7187ac7b98 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Mon, 21 Aug 2017 00:28:01 -0700 Subject: [PATCH 05/16] add doc --- docs/sanic/routing.md | 25 +++++++++++++++++++++++++ sanic/blueprints.py | 1 + 2 files changed, 26 insertions(+) diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index e039e249..f1882684 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -214,3 +214,28 @@ and `recv` methods to send and receive data respectively. WebSocket support requires the [websockets](https://github.com/aaugustin/websockets) package by Aymeric Augustin. + + +## About `strict_slashes` + +You can make `routes` strict to trailing slash or not, it's configurable. + +```python + +# provide default strict_slashes value for all routes +app = Sanic('test_route_strict_slash', strict_slashes=True) + +# you can also overwrite strict_slashes value for specific route +@app.get('/get', strict_slashes=False) +def handler(request): + return text('OK') + +# It also works for blueprints +bp = Blueprint('test_bp_strict_slash', strict_slashes=True) + +@bp.get('/bp/get', strict_slashes=False) +def handler(request): + return text('OK') + +app.blueprint(bp) +``` diff --git a/sanic/blueprints.py b/sanic/blueprints.py index b899481b..235fe909 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -23,6 +23,7 @@ class Blueprint: :param name: unique name of the blueprint :param url_prefix: URL to be prefixed before all route URLs + :param strict_slashes: strict to trailing slash """ self.name = name self.url_prefix = url_prefix From eab809d410ec3dd9291b2fe4e9936fa8b2eeafb0 Mon Sep 17 00:00:00 2001 From: lixxu Date: Mon, 21 Aug 2017 18:05:34 +0800 Subject: [PATCH 06/16] add name option for route building --- docs/sanic/routing.md | 28 +++ sanic/app.py | 64 +++--- sanic/blueprints.py | 88 +++++---- sanic/router.py | 16 +- tests/test_named_routes.py | 391 +++++++++++++++++++++++++++++++++++++ 5 files changed, 517 insertions(+), 70 deletions(-) create mode 100644 tests/test_named_routes.py diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index f1882684..b420a523 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -239,3 +239,31 @@ def handler(request): app.blueprint(bp) ``` + +## User defined route name + +You can pass `name` to change the route name to avoid using the default name (`handler.__name__`). + +```python + +app = Sanic('test_named_route') + +@app.get('/get', name='get_handler') +def handler(request): + return text('OK') + +# then you need use `app.url_for('get_handler')` +# instead of # `app.url_for('handler')` + +# It also works for blueprints +bp = Blueprint('test_named_bp') + +@bp.get('/bp/get', name='get_handler') +def handler(request): + return text('OK') + +app.blueprint(bp) + +# then you need use `app.url_for('test_named_bp.get_handler')` +# instead of `app.url_for('test_named_bp.handler')` +``` diff --git a/sanic/app.py b/sanic/app.py index 53a6a8f6..20c02a5c 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -112,7 +112,7 @@ class Sanic: # Decorator def route(self, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=None, stream=False, version=None): + strict_slashes=None, stream=False, version=None, name=None): """Decorate a function to be registered as a route :param uri: path of the URL @@ -120,6 +120,8 @@ class Sanic: :param host: :param strict_slashes: :param stream: + :param version: + :param name: user defined route name for url_for :return: decorated function """ @@ -139,48 +141,56 @@ class Sanic: handler.is_stream = stream self.router.add(uri=uri, methods=methods, handler=handler, host=host, strict_slashes=strict_slashes, - version=version) + version=version, name=name) return handler return response # Shorthand method decorators - def get(self, uri, host=None, strict_slashes=None, version=None): + def get(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=frozenset({"GET"}), host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) def post(self, uri, host=None, strict_slashes=None, stream=False, - version=None): + version=None, name=None): return self.route(uri, methods=frozenset({"POST"}), host=host, strict_slashes=strict_slashes, stream=stream, - version=version) + version=version, name=name) def put(self, uri, host=None, strict_slashes=None, stream=False, - version=None): + version=None, name=None): return self.route(uri, methods=frozenset({"PUT"}), host=host, strict_slashes=strict_slashes, stream=stream, - version=version) + version=version, name=name) - def head(self, uri, host=None, strict_slashes=None, version=None): + def head(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=frozenset({"HEAD"}), host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) - def options(self, uri, host=None, strict_slashes=None, version=None): + def options(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=frozenset({"OPTIONS"}), host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) def patch(self, uri, host=None, strict_slashes=None, stream=False, - version=None): + version=None, name=None): return self.route(uri, methods=frozenset({"PATCH"}), host=host, strict_slashes=strict_slashes, stream=stream, - version=version) + version=version, name=name) - def delete(self, uri, host=None, strict_slashes=None, version=None): + def delete(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=frozenset({"DELETE"}), host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=None, version=None): + strict_slashes=None, version=None, name=None): """A helper method to register class instance or functions as a handler to the application url routes. @@ -190,6 +200,9 @@ class Sanic: :param methods: list or tuple of methods allowed, these are overridden if using a HTTPMethodView :param host: + :param strict_slashes: + :param version: + :param name: user defined route name for url_for :return: function or class instance """ stream = False @@ -217,12 +230,12 @@ class Sanic: self.route(uri=uri, methods=methods, host=host, strict_slashes=strict_slashes, stream=stream, - version=version)(handler) + version=version, name=name)(handler) return handler # Decorator def websocket(self, uri, host=None, strict_slashes=None, - subprotocols=None): + subprotocols=None, name=None): """Decorate a function to be registered as a websocket route :param uri: path of the URL :param subprotocols: optional list of strings with the supported @@ -265,19 +278,19 @@ class Sanic: self.router.add(uri=uri, handler=websocket_handler, methods=frozenset({'GET'}), host=host, - strict_slashes=strict_slashes) + strict_slashes=strict_slashes, name=name) return handler return response def add_websocket_route(self, handler, uri, host=None, - strict_slashes=None): + strict_slashes=None, name=None): """A helper method to register a function as a websocket route.""" if strict_slashes is None: strict_slashes = self.strict_slashes - return self.websocket(uri, host=host, - strict_slashes=strict_slashes)(handler) + return self.websocket(uri, host=host, strict_slashes=strict_slashes, + name=name)(handler) def enable_websocket(self, enable=True): """Enable or disable the support for websocket. @@ -400,9 +413,8 @@ class Sanic: uri, route = self.router.find_route_by_view_name(view_name) if not uri or not route: - raise URLBuildError( - 'Endpoint with name `{}` was not found'.format( - view_name)) + raise URLBuildError('Endpoint with name `{}` was not found'.format( + view_name)) if uri != '/' and uri.endswith('/'): uri = uri[:-1] diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 235fe909..548aa7ca 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -5,7 +5,7 @@ from sanic.views import CompositionView FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host', - 'strict_slashes', 'stream', 'version']) + 'strict_slashes', 'stream', 'version', 'name']) FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs']) FutureException = namedtuple('Route', ['handler', 'args', 'kwargs']) @@ -53,14 +53,14 @@ class Blueprint: version = future.version or self.version - app.route( - uri=uri[1:] if uri.startswith('//') else uri, - methods=future.methods, - host=future.host or self.host, - strict_slashes=future.strict_slashes, - stream=future.stream, - version=version - )(future.handler) + app.route(uri=uri[1:] if uri.startswith('//') else uri, + methods=future.methods, + host=future.host or self.host, + strict_slashes=future.strict_slashes, + stream=future.stream, + version=version, + name=future.name, + )(future.handler) for future in self.websocket_routes: # attach the blueprint name to the handler so that it can be @@ -68,11 +68,11 @@ class Blueprint: future.handler.__blueprintname__ = self.name # Prepend the blueprint URI prefix if available uri = url_prefix + future.uri if url_prefix else future.uri - app.websocket( - uri=uri, - host=future.host or self.host, - strict_slashes=future.strict_slashes - )(future.handler) + app.websocket(uri=uri, + host=future.host or self.host, + strict_slashes=future.strict_slashes, + name=future.name, + )(future.handler) # Middleware for future in self.middlewares: @@ -100,7 +100,7 @@ class Blueprint: app.listener(event)(listener) def route(self, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=None, stream=False, version=None): + strict_slashes=None, stream=False, version=None, name=None): """Create a blueprint route from a decorated function. :param uri: endpoint at which the route will be accessible. @@ -111,19 +111,24 @@ class Blueprint: def decorator(handler): route = FutureRoute( - handler, uri, methods, host, strict_slashes, stream, version) + handler, uri, methods, host, strict_slashes, stream, version, + name) self.routes.append(route) return handler return decorator def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=None, version=None): + strict_slashes=None, version=None, name=None): """Create a blueprint route from a function. :param handler: function for handling uri requests. Accepts function, or class instance with a view_class method. :param uri: endpoint at which the route will be accessible. :param methods: list of acceptable HTTP methods. + :param host: + :param strict_slashes: + :param version: + :param name: user defined route name for url_for :return: function or class instance """ # Handle HTTPMethodView differently @@ -142,10 +147,12 @@ class Blueprint: methods = handler.handlers.keys() self.route(uri=uri, methods=methods, host=host, - strict_slashes=strict_slashes, version=version)(handler) + strict_slashes=strict_slashes, version=version, + name=name)(handler) return handler - def websocket(self, uri, host=None, strict_slashes=None, version=None): + def websocket(self, uri, host=None, strict_slashes=None, version=None, + name=None): """Create a blueprint websocket route from a decorated function. :param uri: endpoint at which the route will be accessible. @@ -155,12 +162,13 @@ class Blueprint: def decorator(handler): route = FutureRoute(handler, uri, [], host, strict_slashes, - False, version) + False, version, name) self.websocket_routes.append(route) return handler return decorator - def add_websocket_route(self, handler, uri, host=None, version=None): + def add_websocket_route(self, handler, uri, host=None, version=None, + name=None): """Create a blueprint websocket route from a function. :param handler: function for handling uri requests. Accepts function, @@ -168,7 +176,7 @@ class Blueprint: :param uri: endpoint at which the route will be accessible. :return: function or class instance """ - self.websocket(uri=uri, host=host, version=version)(handler) + self.websocket(uri=uri, host=host, version=version, name=name)(handler) return handler def listener(self, event): @@ -214,36 +222,44 @@ class Blueprint: self.statics.append(static) # Shorthand method decorators - def get(self, uri, host=None, strict_slashes=None, version=None): + def get(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=["GET"], host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) def post(self, uri, host=None, strict_slashes=None, stream=False, - version=None): + version=None, name=None): return self.route(uri, methods=["POST"], host=host, strict_slashes=strict_slashes, stream=stream, - version=version) + version=version, name=name) def put(self, uri, host=None, strict_slashes=None, stream=False, - version=None): + version=None, name=None): return self.route(uri, methods=["PUT"], host=host, strict_slashes=strict_slashes, stream=stream, - version=version) + version=version, name=name) - def head(self, uri, host=None, strict_slashes=None, version=None): + def head(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=["HEAD"], host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) - def options(self, uri, host=None, strict_slashes=None, version=None): + def options(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=["OPTIONS"], host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) def patch(self, uri, host=None, strict_slashes=None, stream=False, - version=None): + version=None, name=None): return self.route(uri, methods=["PATCH"], host=host, strict_slashes=strict_slashes, stream=stream, - version=version) + version=version, name=name) - def delete(self, uri, host=None, strict_slashes=None, version=None): + def delete(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=["DELETE"], host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) diff --git a/sanic/router.py b/sanic/router.py index efc48f37..062fecc8 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -99,7 +99,7 @@ class Router: return name, _type, pattern def add(self, uri, methods, handler, host=None, strict_slashes=False, - version=None): + version=None, name=None): """Add a handler to the route list :param uri: path to match @@ -118,7 +118,7 @@ class Router: else: uri = "/".join(["/v{}".format(str(version)), uri]) # add regular version - self._add(uri, methods, handler, host) + self._add(uri, methods, handler, host, name) if strict_slashes: return @@ -135,12 +135,12 @@ class Router: ) # add version with trailing slash if slash_is_missing: - self._add(uri + '/', methods, handler, host) + self._add(uri + '/', methods, handler, host, name) # add version without trailing slash elif without_slash_is_missing: - self._add(uri[:-1], methods, handler, host) + self._add(uri[:-1], methods, handler, host, name) - def _add(self, uri, methods, handler, host=None): + def _add(self, uri, methods, handler, host=None, name=None): """Add a handler to the route list :param uri: path to match @@ -161,7 +161,7 @@ class Router: "host strings, not {!r}".format(host)) for host_ in host: - self.add(uri, methods, handler, host_) + self.add(uri, methods, handler, host_, name) return # Dict for faster lookups of if method allowed @@ -236,9 +236,9 @@ class Router: # if available if hasattr(handler, '__blueprintname__'): handler_name = '{}.{}'.format( - handler.__blueprintname__, handler.__name__) + handler.__blueprintname__, name or handler.__name__) else: - handler_name = getattr(handler, '__name__', None) + handler_name = name or getattr(handler, '__name__', None) route = Route( handler=handler, methods=methods, pattern=pattern, diff --git a/tests/test_named_routes.py b/tests/test_named_routes.py new file mode 100644 index 00000000..730a2206 --- /dev/null +++ b/tests/test_named_routes.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import asyncio +import pytest + +from sanic import Sanic +from sanic.blueprints import Blueprint +from sanic.response import text +from sanic.router import RouteExists, RouteDoesNotExist +from sanic.exceptions import URLBuildError +from sanic.constants import HTTP_METHODS + + +# ------------------------------------------------------------ # +# UTF-8 +# ------------------------------------------------------------ # + +@pytest.mark.parametrize('method', HTTP_METHODS) +def test_versioned_named_routes_get(method): + app = Sanic('test_shorhand_routes_get') + bp = Blueprint('test_bp', url_prefix='/bp') + + method = method.lower() + route_name = 'route_{}'.format(method) + route_name2 = 'route2_{}'.format(method) + + func = getattr(app, method) + if callable(func): + @func('/{}'.format(method), version=1, name=route_name) + def handler(request): + return text('OK') + else: + print(func) + raise + + func = getattr(bp, method) + if callable(func): + @func('/{}'.format(method), version=1, name=route_name2) + def handler2(request): + return text('OK') + + else: + print(func) + raise + + app.blueprint(bp) + + assert app.router.routes_all['/v1/{}'.format(method)].name == route_name + + route = app.router.routes_all['/v1/bp/{}'.format(method)] + assert route.name == 'test_bp.{}'.format(route_name2) + + assert app.url_for(route_name) == '/v1/{}'.format(method) + url = app.url_for('test_bp.{}'.format(route_name2)) + assert url == '/v1/bp/{}'.format(method) + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_shorthand_default_routes_get(): + app = Sanic('test_shorhand_routes_get') + + @app.get('/get') + def handler(request): + return text('OK') + + assert app.router.routes_all['/get'].name == 'handler' + assert app.url_for('handler') == '/get' + + +def test_shorthand_named_routes_get(): + app = Sanic('test_shorhand_routes_get') + bp = Blueprint('test_bp', url_prefix='/bp') + + @app.get('/get', name='route_get') + def handler(request): + return text('OK') + + @bp.get('/get', name='route_bp') + def handler2(request): + return text('Blueprint') + + app.blueprint(bp) + + assert app.router.routes_all['/get'].name == 'route_get' + assert app.url_for('route_get') == '/get' + with pytest.raises(URLBuildError): + app.url_for('handler') + + assert app.router.routes_all['/bp/get'].name == 'test_bp.route_bp' + assert app.url_for('test_bp.route_bp') == '/bp/get' + with pytest.raises(URLBuildError): + app.url_for('test_bp.handler2') + + +def test_shorthand_named_routes_post(): + app = Sanic('test_shorhand_routes_post') + + @app.post('/post', name='route_name') + def handler(request): + return text('OK') + + assert app.router.routes_all['/post'].name == 'route_name' + assert app.url_for('route_name') == '/post' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_shorthand_named_routes_put(): + app = Sanic('test_shorhand_routes_put') + + @app.put('/put', name='route_put') + def handler(request): + assert request.stream is None + return text('OK') + + assert app.is_request_stream is False + assert app.router.routes_all['/put'].name == 'route_put' + assert app.url_for('route_put') == '/put' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_shorthand_named_routes_delete(): + app = Sanic('test_shorhand_routes_delete') + + @app.delete('/delete', name='route_delete') + def handler(request): + assert request.stream is None + return text('OK') + + assert app.is_request_stream is False + assert app.router.routes_all['/delete'].name == 'route_delete' + assert app.url_for('route_delete') == '/delete' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_shorthand_named_routes_patch(): + app = Sanic('test_shorhand_routes_patch') + + @app.patch('/patch', name='route_patch') + def handler(request): + assert request.stream is None + return text('OK') + + assert app.is_request_stream is False + assert app.router.routes_all['/patch'].name == 'route_patch' + assert app.url_for('route_patch') == '/patch' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_shorthand_named_routes_head(): + app = Sanic('test_shorhand_routes_head') + + @app.head('/head', name='route_head') + def handler(request): + assert request.stream is None + return text('OK') + + assert app.is_request_stream is False + assert app.router.routes_all['/head'].name == 'route_head' + assert app.url_for('route_head') == '/head' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_shorthand_named_routes_options(): + app = Sanic('test_shorhand_routes_options') + + @app.options('/options', name='route_options') + def handler(request): + assert request.stream is None + return text('OK') + + assert app.is_request_stream is False + assert app.router.routes_all['/options'].name == 'route_options' + assert app.url_for('route_options') == '/options' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_named_static_routes(): + app = Sanic('test_dynamic_route') + + @app.route('/test', name='route_test') + async def handler1(request): + return text('OK1') + + @app.route('/pizazz', name='route_pizazz') + async def handler2(request): + return text('OK2') + + assert app.router.routes_all['/test'].name == 'route_test' + assert app.router.routes_static['/test'].name == 'route_test' + assert app.url_for('route_test') == '/test' + with pytest.raises(URLBuildError): + app.url_for('handler1') + + assert app.router.routes_all['/pizazz'].name == 'route_pizazz' + assert app.router.routes_static['/pizazz'].name == 'route_pizazz' + assert app.url_for('route_pizazz') == '/pizazz' + with pytest.raises(URLBuildError): + app.url_for('handler2') + + +def test_named_dynamic_route(): + app = Sanic('test_dynamic_route') + + results = [] + + @app.route('/folder/', name='route_dynamic') + async def handler(request, name): + results.append(name) + return text('OK') + + assert app.router.routes_all['/folder/'].name == 'route_dynamic' + assert app.url_for('route_dynamic', name='test') == '/folder/test' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_dynamic_named_route_regex(): + app = Sanic('test_dynamic_route_regex') + + @app.route('/folder/', name='route_re') + async def handler(request, folder_id): + return text('OK') + + route = app.router.routes_all['/folder/'] + assert route.name == 'route_re' + assert app.url_for('route_re', folder_id='test') == '/folder/test' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_dynamic_named_route_path(): + app = Sanic('test_dynamic_route_path') + + @app.route('//info', name='route_dynamic_path') + async def handler(request, path): + return text('OK') + + route = app.router.routes_all['//info'] + assert route.name == 'route_dynamic_path' + assert app.url_for('route_dynamic_path', path='path/1') == '/path/1/info' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_dynamic_named_route_unhashable(): + app = Sanic('test_dynamic_route_unhashable') + + @app.route('/folder//end/', + name='route_unhashable') + async def handler(request, unhashable): + return text('OK') + + route = app.router.routes_all['/folder//end/'] + assert route.name == 'route_unhashable' + url = app.url_for('route_unhashable', unhashable='test/asdf') + assert url == '/folder/test/asdf/end' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_websocket_named_route(): + app = Sanic('test_websocket_route') + ev = asyncio.Event() + + @app.websocket('/ws', name='route_ws') + async def handler(request, ws): + assert ws.subprotocol is None + ev.set() + + assert app.router.routes_all['/ws'].name == 'route_ws' + assert app.url_for('route_ws') == '/ws' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_websocket_named_route_with_subprotocols(): + app = Sanic('test_websocket_route') + results = [] + + @app.websocket('/ws', subprotocols=['foo', 'bar'], name='route_ws') + async def handler(request, ws): + results.append(ws.subprotocol) + + assert app.router.routes_all['/ws'].name == 'route_ws' + assert app.url_for('route_ws') == '/ws' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_static_add_named_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', name='route_test') + app.add_route(handler2, '/test2', name='route_test2') + + assert app.router.routes_all['/test'].name == 'route_test' + assert app.router.routes_static['/test'].name == 'route_test' + assert app.url_for('route_test') == '/test' + with pytest.raises(URLBuildError): + app.url_for('handler1') + + assert app.router.routes_all['/test2'].name == 'route_test2' + assert app.router.routes_static['/test2'].name == 'route_test2' + assert app.url_for('route_test2') == '/test2' + with pytest.raises(URLBuildError): + app.url_for('handler2') + + +def test_dynamic_add_named_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='route_dynamic') + assert app.router.routes_all['/folder/'].name == 'route_dynamic' + assert app.url_for('route_dynamic', name='test') == '/folder/test' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_dynamic_add_named_route_unhashable(): + app = Sanic('test_dynamic_add_route_unhashable') + + async def handler(request, unhashable): + return text('OK') + + app.add_route(handler, '/folder//end/', + name='route_unhashable') + route = app.router.routes_all['/folder//end/'] + assert route.name == 'route_unhashable' + url = app.url_for('route_unhashable', unhashable='folder1') + assert url == '/folder/folder1/end' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_overload_routes(): + app = Sanic('test_dynamic_route') + + @app.route('/overload', methods=['GET'], name='route_first') + async def handler1(request): + return text('OK1') + + @app.route('/overload', methods=['POST', 'PUT'], name='route_second') + async def handler2(request): + return text('OK2') + + @app.route('/overload2', methods=['POST', 'PUT'], name='route_third') + async def handler3(request): + return text('OK2') + + request, response = app.test_client.get(app.url_for('route_first')) + assert response.text == 'OK1' + + request, response = app.test_client.post(app.url_for('route_first')) + assert response.text == 'OK2' + + request, response = app.test_client.put(app.url_for('route_first')) + assert response.text == 'OK2' + + assert app.router.routes_all['/overload'].name == 'route_first' + with pytest.raises(URLBuildError): + app.url_for('handler1') + + with pytest.raises(URLBuildError): + app.url_for('handler2') + + with pytest.raises(URLBuildError): + app.url_for('route_second') + + assert app.url_for('route_third') == '/overload2' + with pytest.raises(URLBuildError): + app.url_for('handler3') From 91f031b66182bd5985f42018aca59e06c99cabde Mon Sep 17 00:00:00 2001 From: jiaxiaolei Date: Mon, 21 Aug 2017 22:28:23 +0800 Subject: [PATCH 07/16] feat(examples): add `authorized_sanic.py` You can check a request if the client is authorized to access a resource by the decorator `authorized` --- examples/authorized_sanic.py | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 examples/authorized_sanic.py diff --git a/examples/authorized_sanic.py b/examples/authorized_sanic.py new file mode 100644 index 00000000..f6b17426 --- /dev/null +++ b/examples/authorized_sanic.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +from sanic import Sanic +from functools import wraps +from sanic.response import json + +app = Sanic() + + +def check_request_for_authorization_status(request): + # Note: Define your check, for instance cookie, session. + flag = True + return flag + + +def authorized(): + def decorator(f): + @wraps(f) + async def decorated_function(request, *args, **kwargs): + # run some method that checks the request + # for the client's authorization status + is_authorized = check_request_for_authorization_status(request) + + if is_authorized: + # the user is authorized. + # run the handler method and return the response + response = await f(request, *args, **kwargs) + return response + else: + # the user is not authorized. + return json({'status': 'not_authorized'}, 403) + return decorated_function + return decorator + + +@app.route("/") +@authorized() +async def test(request): + return json({'status': 'authorized'}) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) From 762b2782eef869584ff45738649451a2c82f26a8 Mon Sep 17 00:00:00 2001 From: lixxu Date: Tue, 22 Aug 2017 14:02:38 +0800 Subject: [PATCH 08/16] use name to define route name for different methods on same url --- docs/sanic/routing.md | 34 +++++++++++++++++++++++++++++++ sanic/router.py | 41 +++++++++++++++++++++----------------- tests/test_named_routes.py | 27 +++++++++++-------------- 3 files changed, 69 insertions(+), 33 deletions(-) diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index b420a523..49b7c0b8 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -266,4 +266,38 @@ app.blueprint(bp) # then you need use `app.url_for('test_named_bp.get_handler')` # instead of `app.url_for('test_named_bp.handler')` + +# different names can be used for same url with different methods + +@app.get('/test', name='route_test') +def handler(request): + return text('OK') + +@app.post('/test', name='route_post') +def handler2(request): + return text('OK POST') + +@app.put('/test', name='route_put') +def handler3(request): + return text('OK PUT') + +# below url are the same, you can use any of them +# '/test' +app.url_for('route_test') +# app.url_for('route_post') +# app.url_for('route_put') + +# for same handler name with different methods +# you need specify the name (it's url_for issue) +@app.get('/get') +def handler(request): + return text('OK') + +@app.post('/post', name='post_handler') +def handler(request): + return text('OK') + +# then +# app.url_for('handler') == '/get' +# app.url_for('post_handler') == '/post' ``` diff --git a/sanic/router.py b/sanic/router.py index 062fecc8..79faaf1e 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -67,6 +67,7 @@ class Router: def __init__(self): self.routes_all = {} + self.routes_names = {} self.routes_static = {} self.routes_dynamic = defaultdict(list) self.routes_always_check = [] @@ -125,13 +126,12 @@ class Router: # Add versions with and without trailing / slash_is_missing = ( - not uri[-1] == '/' - and not self.routes_all.get(uri + '/', False) + not uri[-1] == '/' and not self.routes_all.get(uri + '/', False) ) without_slash_is_missing = ( - uri[-1] == '/' - and not self.routes_all.get(uri[:-1], False) - and not uri == '/' + uri[-1] == '/' and not + self.routes_all.get(uri[:-1], False) and not + uri == '/' ) # add version with trailing slash if slash_is_missing: @@ -229,22 +229,26 @@ class Router: else: route = self.routes_all.get(uri) + # prefix the handler name with the blueprint name + # if available + if hasattr(handler, '__blueprintname__'): + handler_name = '{}.{}'.format( + handler.__blueprintname__, name or handler.__name__) + else: + handler_name = name or getattr(handler, '__name__', None) + if route: route = merge_route(route, methods, handler) else: - # prefix the handler name with the blueprint name - # if available - if hasattr(handler, '__blueprintname__'): - handler_name = '{}.{}'.format( - handler.__blueprintname__, name or handler.__name__) - else: - handler_name = name or getattr(handler, '__name__', None) - route = Route( handler=handler, methods=methods, pattern=pattern, parameters=parameters, name=handler_name, uri=uri) self.routes_all[uri] = route + pairs = self.routes_names.get(handler_name) + if not (pairs and (pairs[0] + '/' == uri or uri + '/' == pairs[0])): + self.routes_names[handler_name] = (uri, route) + if properties['unhashable']: self.routes_always_check.append(route) elif parameters: @@ -265,6 +269,11 @@ class Router: uri = host + uri try: route = self.routes_all.pop(uri) + for handler_name, pairs in self.routes_names.items(): + if pairs[0] == uri: + self.routes_names.pop(handler_name) + break + except KeyError: raise RouteDoesNotExist("Route was not registered: {}".format(uri)) @@ -289,11 +298,7 @@ class Router: if not view_name: return (None, None) - for uri, route in self.routes_all.items(): - if route.name == view_name: - return uri, route - - return (None, None) + return self.routes_names.get(view_name, (None, None)) def get(self, request): """Get a request handler based on the URL of the request, or raises an diff --git a/tests/test_named_routes.py b/tests/test_named_routes.py index 730a2206..ca377e8d 100644 --- a/tests/test_named_routes.py +++ b/tests/test_named_routes.py @@ -7,7 +7,6 @@ import pytest from sanic import Sanic from sanic.blueprints import Blueprint from sanic.response import text -from sanic.router import RouteExists, RouteDoesNotExist from sanic.exceptions import URLBuildError from sanic.constants import HTTP_METHODS @@ -360,11 +359,7 @@ def test_overload_routes(): return text('OK1') @app.route('/overload', methods=['POST', 'PUT'], name='route_second') - async def handler2(request): - return text('OK2') - - @app.route('/overload2', methods=['POST', 'PUT'], name='route_third') - async def handler3(request): + async def handler1(request): return text('OK2') request, response = app.test_client.get(app.url_for('route_first')) @@ -376,16 +371,18 @@ def test_overload_routes(): request, response = app.test_client.put(app.url_for('route_first')) assert response.text == 'OK2' + request, response = app.test_client.get(app.url_for('route_second')) + assert response.text == 'OK1' + + request, response = app.test_client.post(app.url_for('route_second')) + assert response.text == 'OK2' + + request, response = app.test_client.put(app.url_for('route_second')) + assert response.text == 'OK2' + assert app.router.routes_all['/overload'].name == 'route_first' with pytest.raises(URLBuildError): app.url_for('handler1') - with pytest.raises(URLBuildError): - app.url_for('handler2') - - with pytest.raises(URLBuildError): - app.url_for('route_second') - - assert app.url_for('route_third') == '/overload2' - with pytest.raises(URLBuildError): - app.url_for('handler3') + assert app.url_for('route_first') == '/overload' + assert app.url_for('route_second') == app.url_for('route_first') From 35e028cd99aebc500f9cac428ba328c17da8e043 Mon Sep 17 00:00:00 2001 From: xmsun Date: Tue, 22 Aug 2017 16:40:42 +0800 Subject: [PATCH 09/16] fix: error param --- examples/try_everything.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/try_everything.py b/examples/try_everything.py index 2bc1c7b3..76967be1 100644 --- a/examples/try_everything.py +++ b/examples/try_everything.py @@ -18,7 +18,7 @@ def test_sync(request): return response.json({"test": True}) -@app.route("/dynamic//") +@app.route("/dynamic//") def test_params(request, name, i): return response.text("yeehaww {} {}".format(name, i)) From 6038813d0324bd0b7000b993d3ea7ffadc836678 Mon Sep 17 00:00:00 2001 From: Darren Date: Thu, 24 Aug 2017 22:46:39 +0800 Subject: [PATCH 10/16] fix #914, change arguments of Unauthorized.__init__ --- sanic/exceptions.py | 29 ++++++++++++++++++----------- tests/test_exceptions.py | 20 +++++++++++++++++--- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 0edb0562..9663ea7c 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -209,6 +209,7 @@ class Unauthorized(SanicException): Unauthorized exception (401 HTTP status code). :param message: Message describing the exception. + :param status_code: HTTP Status code. :param scheme: Name of the authentication scheme to be used. When present, kwargs is used to complete the WWW-Authentication header. @@ -216,11 +217,13 @@ class Unauthorized(SanicException): Examples:: # With a Basic auth-scheme, realm MUST be present: - raise Unauthorized("Auth required.", "Basic", realm="Restricted Area") + raise Unauthorized("Auth required.", + scheme="Basic", + realm="Restricted Area") # With a Digest auth-scheme, things are a bit more complicated: raise Unauthorized("Auth required.", - "Digest", + scheme="Digest", realm="Restricted Area", qop="auth, auth-int", algorithm="MD5", @@ -228,20 +231,24 @@ class Unauthorized(SanicException): opaque="zyxwvu") # With a Bearer auth-scheme, realm is optional so you can write: - raise Unauthorized("Auth required.", "Bearer") + raise Unauthorized("Auth required.", scheme="Bearer") # or, if you want to specify the realm: - raise Unauthorized("Auth required.", "Bearer", realm="Restricted Area") + raise Unauthorized("Auth required.", + scheme="Bearer", + realm="Restricted Area") """ - def __init__(self, message, scheme, **kwargs): - super().__init__(message) + def __init__(self, message, status_code=None, scheme=None, **kwargs): + super().__init__(message, status_code) - values = ["{!s}={!r}".format(k, v) for k, v in kwargs.items()] - challenge = ', '.join(values) + # if auth-scheme is specified, set "WWW-Authenticate" header + if scheme is not None: + values = ["{!s}={!r}".format(k, v) for k, v in kwargs.items()] + challenge = ', '.join(values) - self.headers = { - "WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip() - } + self.headers = { + "WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip() + } def abort(status_code, message=None): diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 1521c9ed..c535059c 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -31,14 +31,18 @@ def exception_app(): def handler_403(request): raise Forbidden("Forbidden") + @app.route('/401') + def handler_401(request): + raise Unauthorized("Unauthorized") + @app.route('/401/basic') def handler_401_basic(request): - raise Unauthorized("Unauthorized", "Basic", realm="Sanic") + raise Unauthorized("Unauthorized", scheme="Basic", realm="Sanic") @app.route('/401/digest') def handler_401_digest(request): raise Unauthorized("Unauthorized", - "Digest", + scheme="Digest", realm="Sanic", qop="auth, auth-int", algorithm="MD5", @@ -47,12 +51,16 @@ def exception_app(): @app.route('/401/bearer') def handler_401_bearer(request): - raise Unauthorized("Unauthorized", "Bearer") + raise Unauthorized("Unauthorized", scheme="Bearer") @app.route('/invalid') def handler_invalid(request): raise InvalidUsage("OK") + @app.route('/abort/401') + def handler_invalid(request): + abort(401) + @app.route('/abort') def handler_invalid(request): abort(500) @@ -124,6 +132,9 @@ def test_forbidden_exception(exception_app): def test_unauthorized_exception(exception_app): """Test the built-in Unauthorized exception""" + request, response = exception_app.test_client.get('/401') + assert response.status == 401 + request, response = exception_app.test_client.get('/401/basic') assert response.status == 401 assert response.headers.get('WWW-Authenticate') is not None @@ -186,5 +197,8 @@ def test_exception_in_exception_handler_debug_off(exception_app): def test_abort(exception_app): """Test the abort function""" + request, response = exception_app.test_client.get('/abort/401') + assert response.status == 401 + request, response = exception_app.test_client.get('/abort') assert response.status == 500 From 0a72168f8f925f593a6ae0e8f5e4f26a6cfcd53b Mon Sep 17 00:00:00 2001 From: Timur Date: Tue, 29 Aug 2017 23:05:57 +0300 Subject: [PATCH 11/16] Example logging X-Request-Id transparently --- examples/log_request_id.py | 86 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 examples/log_request_id.py diff --git a/examples/log_request_id.py b/examples/log_request_id.py new file mode 100644 index 00000000..a6dba418 --- /dev/null +++ b/examples/log_request_id.py @@ -0,0 +1,86 @@ +''' +Based on example from https://github.com/Skyscanner/aiotask-context +and `examples/{override_logging,run_async}.py`. + +Needs https://github.com/Skyscanner/aiotask-context/tree/52efbc21e2e1def2d52abb9a8e951f3ce5e6f690 or newer + +$ pip install git+https://github.com/Skyscanner/aiotask-context.git +''' + +import asyncio +import uuid +import logging +from signal import signal, SIGINT + +from sanic import Sanic +from sanic import response + +import uvloop +import aiotask_context as context + +log = logging.getLogger(__name__) + + +class RequestIdFilter(logging.Filter): + def filter(self, record): + record.request_id = context.get('X-Request-ID') + return True + + +LOG_SETTINGS = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'level': 'DEBUG', + 'formatter': 'default', + 'filters': ['requestid'], + }, + }, + 'filters': { + 'requestid': { + '()': RequestIdFilter, + }, + }, + 'formatters': { + 'default': { + 'format': '%(asctime)s %(levelname)s %(name)s:%(lineno)d %(request_id)s | %(message)s', + }, + }, + 'loggers': { + '': { + 'level': 'DEBUG', + 'handlers': ['console'], + 'propagate': True + }, + } +} + + +app = Sanic(__name__, log_config=LOG_SETTINGS) + + +@app.middleware('request') +async def set_request_id(request): + request_id = request.headers.get('X-Request-ID') or str(uuid.uuid4()) + context.set("X-Request-ID", request_id) + + +@app.route("/") +async def test(request): + log.debug('X-Request-ID: %s', context.get('X-Request-ID')) + log.info('Hello from test!') + return response.json({"test": True}) + + +if __name__ == '__main__': + asyncio.set_event_loop(uvloop.new_event_loop()) + server = app.create_server(host="0.0.0.0", port=8000) + loop = asyncio.get_event_loop() + loop.set_task_factory(context.task_factory) + task = asyncio.ensure_future(server) + try: + loop.run_forever() + except: + loop.stop() From f49554aa5723d8730dbd23c884b5d207a5f32d3b Mon Sep 17 00:00:00 2001 From: Maksim Anisenkov Date: Wed, 30 Aug 2017 15:28:12 +0200 Subject: [PATCH 12/16] Fix LICENSE date and name --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 63b4b681..74ee7987 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) [year] [fullname] +Copyright (c) 2016-present Channel Cat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. From c59a8a60eb7ceaf3b8f93a9abcd1db639338ecb9 Mon Sep 17 00:00:00 2001 From: Tim Mundt Date: Tue, 5 Sep 2017 09:53:33 +0200 Subject: [PATCH 13/16] make the prefix for environment variables alterable --- sanic/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index 6ffcf7a1..6a53e476 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -195,14 +195,14 @@ class Config(dict): if key.isupper(): self[key] = getattr(obj, key) - def load_environment_vars(self): + def load_environment_vars(self, prefix=SANIC_PREFIX): """ - Looks for any ``SANIC_`` prefixed environment variables and applies + Looks for prefixed environment variables and applies them to the configuration if present. """ for k, v in os.environ.items(): - if k.startswith(SANIC_PREFIX): - _, config_key = k.split(SANIC_PREFIX, 1) + if k.startswith(prefix): + _, config_key = k.split(prefix, 1) try: self[config_key] = int(v) except ValueError: From 97d8b9e90806e735104657e658f83c0e94215085 Mon Sep 17 00:00:00 2001 From: Tim Mundt Date: Tue, 5 Sep 2017 10:41:55 +0200 Subject: [PATCH 14/16] documentation for env var prefix; allow passing in the prefix through the app constructor --- docs/sanic/config.md | 10 ++++++++-- sanic/config.py | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/sanic/config.md b/docs/sanic/config.md index ab63f7c8..01ee7fc3 100644 --- a/docs/sanic/config.md +++ b/docs/sanic/config.md @@ -29,9 +29,15 @@ In general the convention is to only have UPPERCASE configuration parameters. Th There are several ways how to load configuration. -### From environment variables. +### From Environment Variables -Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically. You can pass the `load_env` boolean to the Sanic constructor to override that: +Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically and fed into the `REQUEST_TIMEOUT` config variable. You can pass a different prefix to Sanic: + +```python +app = Sanic(load_env='MYAPP_') +``` + +Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead: ```python app = Sanic(load_env=False) diff --git a/sanic/config.py b/sanic/config.py index 6a53e476..18514425 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -131,7 +131,8 @@ class Config(dict): self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec if load_env: - self.load_environment_vars() + prefix = SANIC_PREFIX if load_env == True else load_env + self.load_environment_vars(prefix=load_env) def __getattr__(self, attr): try: From 9572ecc5ea2f6346c9a7879b1093719fd8b6b495 Mon Sep 17 00:00:00 2001 From: Tim Mundt Date: Tue, 5 Sep 2017 10:58:48 +0200 Subject: [PATCH 15/16] test for env var prefix --- sanic/config.py | 2 +- tests/test_config.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index 18514425..853e4a99 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -132,7 +132,7 @@ class Config(dict): if load_env: prefix = SANIC_PREFIX if load_env == True else load_env - self.load_environment_vars(prefix=load_env) + self.load_environment_vars(prefix=prefix) def __getattr__(self, attr): try: diff --git a/tests/test_config.py b/tests/test_config.py index aa7a0e4d..e393d02b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -19,15 +19,21 @@ def test_load_from_object(): def test_auto_load_env(): environ["SANIC_TEST_ANSWER"] = "42" app = Sanic() - assert app.config.TEST_ANSWER == "42" + assert app.config.TEST_ANSWER == 42 del environ["SANIC_TEST_ANSWER"] -def test_auto_load_env(): +def test_dont_load_env(): environ["SANIC_TEST_ANSWER"] = "42" app = Sanic(load_env=False) assert getattr(app.config, 'TEST_ANSWER', None) == None del environ["SANIC_TEST_ANSWER"] +def test_load_env_prefix(): + environ["MYAPP_TEST_ANSWER"] = "42" + app = Sanic(load_env='MYAPP_') + assert app.config.TEST_ANSWER == 42 + del environ["MYAPP_TEST_ANSWER"] + def test_load_from_file(): app = Sanic('test_load_from_file') config = b""" From e2e25eb751beb4879be6b64ddb2ded6c48301c96 Mon Sep 17 00:00:00 2001 From: Tim Mundt Date: Tue, 5 Sep 2017 11:05:31 +0200 Subject: [PATCH 16/16] fixed flake convention --- sanic/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/config.py b/sanic/config.py index 853e4a99..e8846465 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -131,7 +131,7 @@ class Config(dict): self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec if load_env: - prefix = SANIC_PREFIX if load_env == True else load_env + prefix = SANIC_PREFIX if load_env is True else load_env self.load_environment_vars(prefix=prefix) def __getattr__(self, attr):