From ef81a9f54703aa0adaa8132cc54855353c9a6e33 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 20 Aug 2017 23:11:38 -0700 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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