commit
0db49f7520
|
@ -21,12 +21,12 @@ Hello World Example
|
|||
|
||||
app = Sanic()
|
||||
|
||||
@app.route("/")
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
return json({'hello': 'world'})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
|
|
@ -51,6 +51,73 @@ will look like:
|
|||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
|
||||
```
|
||||
|
||||
## Blueprint groups and nesting
|
||||
|
||||
Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example:
|
||||
|
||||
```
|
||||
api/
|
||||
├──content/
|
||||
│ ├──authors.py
|
||||
│ ├──static.py
|
||||
│ └──__init__.py
|
||||
├──info.py
|
||||
└──__init__.py
|
||||
app.py
|
||||
```
|
||||
|
||||
Initialization of this app's blueprint hierarchy could go as follows:
|
||||
|
||||
```python
|
||||
# api/content/authors.py
|
||||
from sanic import Blueprint
|
||||
|
||||
authors = Blueprint('content_authors', url_prefix='/authors')
|
||||
```
|
||||
```python
|
||||
# api/content/static.py
|
||||
from sanic import Blueprint
|
||||
|
||||
static = Blueprint('content_static', url_prefix='/static')
|
||||
```
|
||||
```python
|
||||
# api/content/__init__.py
|
||||
from sanic import Blueprint
|
||||
|
||||
from .static import static
|
||||
from .authors import authors
|
||||
|
||||
content = Blueprint.group(assets, authors, url_prefix='/content')
|
||||
```
|
||||
```python
|
||||
# api/info.py
|
||||
from sanic import Blueprint
|
||||
|
||||
info = Blueprint('info', url_prefix='/info')
|
||||
```
|
||||
```python
|
||||
# api/__init__.py
|
||||
from sanic import Blueprint
|
||||
|
||||
from .content import content
|
||||
from .info import info
|
||||
|
||||
api = Blueprint.group(content, info, url_prefix='/api')
|
||||
```
|
||||
|
||||
And registering these blueprints in `app.py` can now be done like so:
|
||||
|
||||
```python
|
||||
# app.py
|
||||
from sanic import Sanic
|
||||
|
||||
from .api import api
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
app.blueprint(api)
|
||||
```
|
||||
|
||||
## Using blueprints
|
||||
|
||||
Blueprints have much the same functionality as an application instance.
|
||||
|
|
|
@ -73,6 +73,8 @@ The following variables are accessible as properties on `Request` objects:
|
|||
|
||||
- `headers` (dict) - A case-insensitive dictionary that contains the request headers.
|
||||
|
||||
- `method` (str) - HTTP method of the request (ie `GET`, `POST`).
|
||||
|
||||
- `ip` (str) - IP address of the requester.
|
||||
|
||||
- `port` (str) - Port address of the requester.
|
||||
|
|
16
sanic/app.py
16
sanic/app.py
|
@ -372,10 +372,14 @@ class Sanic:
|
|||
def blueprint(self, blueprint, **options):
|
||||
"""Register a blueprint on the application.
|
||||
|
||||
:param blueprint: Blueprint object
|
||||
:param blueprint: Blueprint object or (list, tuple) thereof
|
||||
:param options: option dictionary with blueprint defaults
|
||||
:return: Nothing
|
||||
"""
|
||||
if isinstance(blueprint, (list, tuple)):
|
||||
for item in blueprint:
|
||||
self.blueprint(item, **options)
|
||||
return
|
||||
if blueprint.name in self.blueprints:
|
||||
assert self.blueprints[blueprint.name] is blueprint, \
|
||||
'A blueprint with the name "%s" is already registered. ' \
|
||||
|
@ -577,13 +581,17 @@ class Sanic:
|
|||
if isawaitable(response):
|
||||
response = await response
|
||||
except Exception as e:
|
||||
if self.debug:
|
||||
if isinstance(e, SanicException):
|
||||
response = self.error_handler.default(request=request,
|
||||
exception=e)
|
||||
elif self.debug:
|
||||
response = HTTPResponse(
|
||||
"Error while handling error: {}\nStack: {}".format(
|
||||
e, format_exc()))
|
||||
e, format_exc()), status=500)
|
||||
else:
|
||||
response = HTTPResponse(
|
||||
"An error occurred while handling an error")
|
||||
"An error occurred while handling an error",
|
||||
status=500)
|
||||
finally:
|
||||
# -------------------------------------------- #
|
||||
# Response Middleware
|
||||
|
|
|
@ -14,7 +14,6 @@ FutureStatic = namedtuple('Route',
|
|||
|
||||
|
||||
class Blueprint:
|
||||
|
||||
def __init__(self, name,
|
||||
url_prefix=None,
|
||||
host=None, version=None,
|
||||
|
@ -38,6 +37,27 @@ class Blueprint:
|
|||
self.version = version
|
||||
self.strict_slashes = strict_slashes
|
||||
|
||||
@staticmethod
|
||||
def group(*blueprints, url_prefix=''):
|
||||
"""Create a list of blueprints, optionally
|
||||
grouping them under a general URL prefix.
|
||||
|
||||
:param blueprints: blueprints to be registered as a group
|
||||
:param url_prefix: URL route to be prepended to all sub-prefixes
|
||||
"""
|
||||
def chain(nested):
|
||||
"""itertools.chain() but leaves strings untouched"""
|
||||
for i in nested:
|
||||
if isinstance(i, (list, tuple)):
|
||||
yield from chain(i)
|
||||
else:
|
||||
yield i
|
||||
bps = []
|
||||
for bp in chain(blueprints):
|
||||
bp.url_prefix = url_prefix + bp.url_prefix
|
||||
bps.append(bp)
|
||||
return bps
|
||||
|
||||
def register(self, app, options):
|
||||
"""Register the blueprint to the sanic app."""
|
||||
|
||||
|
|
|
@ -182,7 +182,7 @@ class Request(dict):
|
|||
@property
|
||||
def socket(self):
|
||||
if not hasattr(self, '_socket'):
|
||||
self._get_socket()
|
||||
self._get_address()
|
||||
return self._socket
|
||||
|
||||
def _get_address(self):
|
||||
|
|
|
@ -234,11 +234,11 @@ class Router:
|
|||
if properties['unhashable']:
|
||||
routes_to_check = self.routes_always_check
|
||||
ndx, route = self.check_dynamic_route_exists(
|
||||
pattern, routes_to_check)
|
||||
pattern, routes_to_check, parameters)
|
||||
else:
|
||||
routes_to_check = self.routes_dynamic[url_hash(uri)]
|
||||
ndx, route = self.check_dynamic_route_exists(
|
||||
pattern, routes_to_check)
|
||||
pattern, routes_to_check, parameters)
|
||||
if ndx != -1:
|
||||
# Pop the ndx of the route, no dups of the same route
|
||||
routes_to_check.pop(ndx)
|
||||
|
@ -285,9 +285,9 @@ class Router:
|
|||
self.routes_static[uri] = route
|
||||
|
||||
@staticmethod
|
||||
def check_dynamic_route_exists(pattern, routes_to_check):
|
||||
def check_dynamic_route_exists(pattern, routes_to_check, parameters):
|
||||
for ndx, route in enumerate(routes_to_check):
|
||||
if route.pattern == pattern:
|
||||
if route.pattern == pattern and route.parameters == parameters:
|
||||
return ndx, route
|
||||
else:
|
||||
return -1, None
|
||||
|
|
|
@ -5,7 +5,7 @@ from functools import partial
|
|||
from inspect import isawaitable
|
||||
from multiprocessing import Process
|
||||
from signal import (
|
||||
SIGTERM, SIGINT,
|
||||
SIGTERM, SIGINT, SIG_IGN,
|
||||
signal as signal_func,
|
||||
Signals
|
||||
)
|
||||
|
@ -20,9 +20,10 @@ from httptools import HttpRequestParser
|
|||
from httptools.parser.errors import HttpParserError
|
||||
|
||||
try:
|
||||
import uvloop as async_loop
|
||||
import uvloop
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
except ImportError:
|
||||
async_loop = asyncio
|
||||
pass
|
||||
|
||||
from sanic.log import logger, access_logger
|
||||
from sanic.response import HTTPResponse
|
||||
|
@ -509,11 +510,11 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
request_timeout=60, response_timeout=60, keep_alive_timeout=5,
|
||||
ssl=None, sock=None, request_max_size=None, reuse_port=False,
|
||||
loop=None, protocol=HttpProtocol, backlog=100,
|
||||
register_sys_signals=True, run_async=False, connections=None,
|
||||
signal=Signal(), request_class=None, access_log=True,
|
||||
keep_alive=True, is_request_stream=False, router=None,
|
||||
websocket_max_size=None, websocket_max_queue=None, state=None,
|
||||
graceful_shutdown_timeout=15.0):
|
||||
register_sys_signals=True, run_multiple=False, run_async=False,
|
||||
connections=None, signal=Signal(), request_class=None,
|
||||
access_log=True, keep_alive=True, is_request_stream=False,
|
||||
router=None, websocket_max_size=None, websocket_max_queue=None,
|
||||
state=None, graceful_shutdown_timeout=15.0):
|
||||
"""Start asynchronous HTTP Server on an individual process.
|
||||
|
||||
:param host: Address to host on
|
||||
|
@ -547,7 +548,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
:return: Nothing
|
||||
"""
|
||||
if not run_async:
|
||||
loop = async_loop.new_event_loop()
|
||||
# create new event_loop after fork
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
if debug:
|
||||
|
@ -603,9 +605,14 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
|
||||
trigger_events(after_start, loop)
|
||||
|
||||
# Ignore SIGINT when run_multiple
|
||||
if run_multiple:
|
||||
signal_func(SIGINT, SIG_IGN)
|
||||
|
||||
# Register signals for graceful termination
|
||||
if register_sys_signals:
|
||||
for _signal in (SIGINT, SIGTERM):
|
||||
_singals = (SIGTERM,) if run_multiple else (SIGINT, SIGTERM)
|
||||
for _signal in _singals:
|
||||
try:
|
||||
loop.add_signal_handler(_signal, loop.stop)
|
||||
except NotImplementedError:
|
||||
|
@ -668,6 +675,7 @@ def serve_multiple(server_settings, workers):
|
|||
:return:
|
||||
"""
|
||||
server_settings['reuse_port'] = True
|
||||
server_settings['run_multiple'] = True
|
||||
|
||||
# Handling when custom socket is not provided.
|
||||
if server_settings.get('sock') is None:
|
||||
|
@ -682,12 +690,13 @@ def serve_multiple(server_settings, workers):
|
|||
def sig_handler(signal, frame):
|
||||
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
||||
for process in processes:
|
||||
os.kill(process.pid, SIGINT)
|
||||
os.kill(process.pid, SIGTERM)
|
||||
|
||||
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
|
||||
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
|
||||
|
||||
processes = []
|
||||
|
||||
for _ in range(workers):
|
||||
process = Process(target=serve, kwargs=server_settings)
|
||||
process.daemon = True
|
||||
|
|
|
@ -446,3 +446,44 @@ def test_bp_shorthand():
|
|||
'Sec-WebSocket-Version': '13'})
|
||||
assert response.status == 101
|
||||
assert ev.is_set()
|
||||
|
||||
def test_bp_group():
|
||||
app = Sanic('test_nested_bp_groups')
|
||||
|
||||
deep_0 = Blueprint('deep_0', url_prefix='/deep')
|
||||
deep_1 = Blueprint('deep_1', url_prefix = '/deep1')
|
||||
|
||||
@deep_0.route('/')
|
||||
def handler(request):
|
||||
return text('D0_OK')
|
||||
|
||||
@deep_1.route('/bottom')
|
||||
def handler(request):
|
||||
return text('D1B_OK')
|
||||
|
||||
mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid')
|
||||
mid_1 = Blueprint('mid_tier', url_prefix='/mid1')
|
||||
|
||||
@mid_1.route('/')
|
||||
def handler(request):
|
||||
return text('M1_OK')
|
||||
|
||||
top = Blueprint.group(mid_0, mid_1)
|
||||
|
||||
app.blueprint(top)
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
return text('TOP_OK')
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.text == 'TOP_OK'
|
||||
|
||||
request, response = app.test_client.get('/mid1')
|
||||
assert response.text == 'M1_OK'
|
||||
|
||||
request, response = app.test_client.get('/mid/deep')
|
||||
assert response.text == 'D0_OK'
|
||||
|
||||
request, response = app.test_client.get('/mid/deep1/bottom')
|
||||
assert response.text == 'D1B_OK'
|
||||
|
|
|
@ -23,4 +23,3 @@ def test_multiprocessing():
|
|||
app.run(HOST, app.test_port, workers=num_workers)
|
||||
|
||||
assert len(process_list) == num_workers
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import asyncio
|
|||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.response import text, json
|
||||
from sanic.router import RouteExists, RouteDoesNotExist
|
||||
from sanic.constants import HTTP_METHODS
|
||||
|
||||
|
@ -907,3 +907,27 @@ def test_unicode_routes():
|
|||
|
||||
request, response = app.test_client.get('/overload/你好')
|
||||
assert response.text == 'OK2 你好'
|
||||
|
||||
|
||||
def test_uri_with_different_method_and_different_params():
|
||||
app = Sanic('test_uri')
|
||||
|
||||
@app.route('/ads/<ad_id>', methods=['GET'])
|
||||
async def ad_get(request, ad_id):
|
||||
return json({'ad_id': ad_id})
|
||||
|
||||
@app.route('/ads/<action>', methods=['POST'])
|
||||
async def ad_post(request, action):
|
||||
return json({'action': action})
|
||||
|
||||
request, response = app.test_client.get('/ads/1234')
|
||||
assert response.status == 200
|
||||
assert response.json == {
|
||||
'ad_id': '1234'
|
||||
}
|
||||
|
||||
request, response = app.test_client.post('/ads/post')
|
||||
assert response.status == 200
|
||||
assert response.json == {
|
||||
'action': 'post'
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user