Merge pull request #850 from r0fls/versioning

Versioning
This commit is contained in:
Raphael Deem 2017-07-12 22:37:14 -07:00 committed by GitHub
commit 8e62b3e438
6 changed files with 170 additions and 45 deletions

50
docs/sanic/versioning.md Normal file
View File

@ -0,0 +1,50 @@
# Versioning
You can pass the `version` keyword to the route decorators, or to a blueprint initializer. It will result in the `v{version}` url prefix where `{version}` is the version number.
## Per route
You can pass a version number to the routes directly.
```python
from sanic import response
@app.route('/text', verion=1)
def handle_request(request):
return response.text('Hello world! Version 1')
@app.route('/text', verion=2)
def handle_request(request):
return response.text('Hello world! Version 2')
app.run(port=80)
```
Then with curl:
```bash
curl localhost/v1/text
curl localhost/v2/text
```
## Global blueprint version
You can also pass a version number to the blueprint, which will apply to all routes.
```python
from sanic import response
from sanic.blueprints import Blueprint
bp = Blueprint('test', version=1)
@bp.route('/html')
def handle_request(request):
return response.html('<p>Hello world!</p>')
```
Then with curl:
```bash
curl localhost/v1/html
```

View File

@ -113,7 +113,7 @@ class Sanic:
# Decorator
def route(self, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False, stream=False):
strict_slashes=False, stream=False, version=None):
"""Decorate a function to be registered as a route
:param uri: path of the URL
@ -136,42 +136,49 @@ class Sanic:
if stream:
handler.is_stream = stream
self.router.add(uri=uri, methods=methods, handler=handler,
host=host, strict_slashes=strict_slashes)
host=host, strict_slashes=strict_slashes,
version=version)
return handler
return response
# Shorthand method decorators
def get(self, uri, host=None, strict_slashes=False):
def get(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=frozenset({"GET"}), host=host,
strict_slashes=strict_slashes)
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=False, stream=False,
version=None):
return self.route(uri, methods=frozenset({"POST"}), host=host,
strict_slashes=strict_slashes, stream=stream)
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=False, stream=False,
version=None):
return self.route(uri, methods=frozenset({"PUT"}), host=host,
strict_slashes=strict_slashes, stream=stream)
strict_slashes=strict_slashes, stream=stream,
version=version)
def head(self, uri, host=None, strict_slashes=False):
def head(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=frozenset({"HEAD"}), host=host,
strict_slashes=strict_slashes)
strict_slashes=strict_slashes, version=version)
def options(self, uri, host=None, strict_slashes=False):
def options(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
strict_slashes=strict_slashes)
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=False, stream=False,
version=None):
return self.route(uri, methods=frozenset({"PATCH"}), host=host,
strict_slashes=strict_slashes, stream=stream)
strict_slashes=strict_slashes, stream=stream,
version=version)
def delete(self, uri, host=None, strict_slashes=False):
def delete(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=frozenset({"DELETE"}), host=host,
strict_slashes=strict_slashes)
strict_slashes=strict_slashes, version=version)
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False):
strict_slashes=False, version=None):
"""A helper method to register class instance or
functions as a handler to the application url
routes.
@ -204,7 +211,8 @@ class Sanic:
break
self.route(uri=uri, methods=methods, host=host,
strict_slashes=strict_slashes, stream=stream)(handler)
strict_slashes=strict_slashes, stream=stream,
version=version)(handler)
return handler
# Decorator

View File

@ -4,8 +4,8 @@ from sanic.constants import HTTP_METHODS
from sanic.views import CompositionView
FutureRoute = namedtuple('Route',
['handler', 'uri', 'methods',
'host', 'strict_slashes', 'stream'])
['handler', 'uri', 'methods', 'host',
'strict_slashes', 'stream', 'version'])
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
@ -14,7 +14,7 @@ FutureStatic = namedtuple('Route',
class Blueprint:
def __init__(self, name, url_prefix=None, host=None):
def __init__(self, name, url_prefix=None, host=None, version=None):
"""Create a new blueprint
:param name: unique name of the blueprint
@ -30,6 +30,7 @@ class Blueprint:
self.listeners = defaultdict(list)
self.middlewares = []
self.statics = []
self.version = version
def register(self, app, options):
"""Register the blueprint to the sanic app."""
@ -43,12 +44,16 @@ 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
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
stream=future.stream,
version=version
)(future.handler)
for future in self.websocket_routes:
@ -89,7 +94,7 @@ class Blueprint:
app.listener(event)(listener)
def route(self, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False, stream=False):
strict_slashes=False, stream=False, version=None):
"""Create a blueprint route from a decorated function.
:param uri: endpoint at which the route will be accessible.
@ -97,13 +102,13 @@ class Blueprint:
"""
def decorator(handler):
route = FutureRoute(
handler, uri, methods, host, strict_slashes, stream)
handler, uri, methods, host, strict_slashes, stream, version)
self.routes.append(route)
return handler
return decorator
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
strict_slashes=False):
strict_slashes=False, version=None):
"""Create a blueprint route from a function.
:param handler: function for handling uri requests. Accepts function,
@ -125,21 +130,22 @@ class Blueprint:
methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host,
strict_slashes=strict_slashes)(handler)
strict_slashes=strict_slashes, version=version)(handler)
return handler
def websocket(self, uri, host=None, strict_slashes=False):
def websocket(self, uri, host=None, strict_slashes=False, version=None):
"""Create a blueprint websocket route from a decorated function.
:param uri: endpoint at which the route will be accessible.
"""
def decorator(handler):
route = FutureRoute(handler, uri, [], host, strict_slashes, False)
route = FutureRoute(handler, uri, [], host, strict_slashes,
False, version)
self.websocket_routes.append(route)
return handler
return decorator
def add_websocket_route(self, handler, uri, host=None):
def add_websocket_route(self, handler, uri, host=None, version=None):
"""Create a blueprint websocket route from a function.
:param handler: function for handling uri requests. Accepts function,
@ -147,7 +153,7 @@ class Blueprint:
:param uri: endpoint at which the route will be accessible.
:return: function or class instance
"""
self.websocket(uri=uri, host=host)(handler)
self.websocket(uri=uri, host=host, version=version)(handler)
return handler
def listener(self, event):
@ -193,30 +199,36 @@ class Blueprint:
self.statics.append(static)
# Shorthand method decorators
def get(self, uri, host=None, strict_slashes=False):
def get(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=["GET"], host=host,
strict_slashes=strict_slashes)
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=False, stream=False,
version=None):
return self.route(uri, methods=["POST"], host=host,
strict_slashes=strict_slashes, stream=stream)
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=False, stream=False,
version=None):
return self.route(uri, methods=["PUT"], host=host,
strict_slashes=strict_slashes, stream=stream)
strict_slashes=strict_slashes, stream=stream,
version=version)
def head(self, uri, host=None, strict_slashes=False):
def head(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=["HEAD"], host=host,
strict_slashes=strict_slashes)
strict_slashes=strict_slashes, version=version)
def options(self, uri, host=None, strict_slashes=False):
def options(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=["OPTIONS"], host=host,
strict_slashes=strict_slashes)
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=False, stream=False,
version=None):
return self.route(uri, methods=["PATCH"], host=host,
strict_slashes=strict_slashes, stream=stream)
strict_slashes=strict_slashes, stream=stream,
version=version)
def delete(self, uri, host=None, strict_slashes=False):
def delete(self, uri, host=None, strict_slashes=False, version=None):
return self.route(uri, methods=["DELETE"], host=host,
strict_slashes=strict_slashes)
strict_slashes=strict_slashes, version=version)

View File

@ -98,7 +98,8 @@ class Router:
return name, _type, pattern
def add(self, uri, methods, handler, host=None, strict_slashes=False):
def add(self, uri, methods, handler, host=None, strict_slashes=False,
version=None):
"""Add a handler to the route list
:param uri: path to match
@ -107,8 +108,15 @@ class Router:
:param handler: request handler function.
When executed, it should provide a response object.
:param strict_slashes: strict to trailing slash
:param version: current version of the route or blueprint. See
docs for further details.
:return: Nothing
"""
if version is not None:
if uri.startswith('/'):
uri = "/".join(["/v{}".format(str(version)), uri[1:]])
else:
uri = "/".join(["/v{}".format(str(version)), uri])
# add regular version
self._add(uri, methods, handler, host)

View File

@ -1,16 +1,42 @@
import asyncio
import inspect
import pytest
from sanic import Sanic
from sanic.blueprints import Blueprint
from sanic.response import json, text
from sanic.exceptions import NotFound, ServerError, InvalidUsage
from sanic.constants import HTTP_METHODS
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_versioned_routes_get(method):
app = Sanic('test_shorhand_routes_get')
bp = Blueprint('test_text')
method = method.lower()
func = getattr(bp, method)
if callable(func):
@func('/{}'.format(method), version=1)
def handler(request):
return text('OK')
else:
print(func)
raise
app.blueprint(bp)
client_method = getattr(app.test_client, method)
request, response = client_method('/v1/{}'.format(method))
assert response.status == 200
def test_bp():
app = Sanic('test_text')
bp = Blueprint('test_text')

View File

@ -4,12 +4,33 @@ import pytest
from sanic import Sanic
from sanic.response import text
from sanic.router import RouteExists, RouteDoesNotExist
from sanic.constants import HTTP_METHODS
# ------------------------------------------------------------ #
# UTF-8
# ------------------------------------------------------------ #
@pytest.mark.parametrize('method', HTTP_METHODS)
def test_versioned_routes_get(method):
app = Sanic('test_shorhand_routes_get')
method = method.lower()
func = getattr(app, method)
if callable(func):
@func('/{}'.format(method), version=1)
def handler(request):
return text('OK')
else:
print(func)
raise
client_method = getattr(app.test_client, method)
request, response = client_method('/v1/{}'.format(method))
assert response.status== 200
def test_shorthand_routes_get():
app = Sanic('test_shorhand_routes_get')