Merge pull request #4 from channelcat/master

Update
This commit is contained in:
howie.hu 2018-01-22 13:36:48 +08:00 committed by GitHub
commit 0db49f7520
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 197 additions and 27 deletions

View File

@ -21,12 +21,12 @@ Hello World Example
app = Sanic() app = Sanic()
@app.route("/") @app.route('/')
async def test(request): async def test(request):
return json({"hello": "world"}) return json({'hello': 'world'})
if __name__ == "__main__": if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000) app.run(host='0.0.0.0', port=8000)
Installation Installation
------------ ------------

View File

@ -51,6 +51,73 @@ will look like:
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])] [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 ## Using blueprints
Blueprints have much the same functionality as an application instance. Blueprints have much the same functionality as an application instance.

View File

@ -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. - `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. - `ip` (str) - IP address of the requester.
- `port` (str) - Port address of the requester. - `port` (str) - Port address of the requester.

View File

@ -372,10 +372,14 @@ class Sanic:
def blueprint(self, blueprint, **options): def blueprint(self, blueprint, **options):
"""Register a blueprint on the application. """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 :param options: option dictionary with blueprint defaults
:return: Nothing :return: Nothing
""" """
if isinstance(blueprint, (list, tuple)):
for item in blueprint:
self.blueprint(item, **options)
return
if blueprint.name in self.blueprints: if blueprint.name in self.blueprints:
assert self.blueprints[blueprint.name] is blueprint, \ assert self.blueprints[blueprint.name] is blueprint, \
'A blueprint with the name "%s" is already registered. ' \ 'A blueprint with the name "%s" is already registered. ' \
@ -577,13 +581,17 @@ class Sanic:
if isawaitable(response): if isawaitable(response):
response = await response response = await response
except Exception as e: 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( response = HTTPResponse(
"Error while handling error: {}\nStack: {}".format( "Error while handling error: {}\nStack: {}".format(
e, format_exc())) e, format_exc()), status=500)
else: else:
response = HTTPResponse( response = HTTPResponse(
"An error occurred while handling an error") "An error occurred while handling an error",
status=500)
finally: finally:
# -------------------------------------------- # # -------------------------------------------- #
# Response Middleware # Response Middleware

View File

@ -14,7 +14,6 @@ FutureStatic = namedtuple('Route',
class Blueprint: class Blueprint:
def __init__(self, name, def __init__(self, name,
url_prefix=None, url_prefix=None,
host=None, version=None, host=None, version=None,
@ -38,6 +37,27 @@ class Blueprint:
self.version = version self.version = version
self.strict_slashes = strict_slashes 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): def register(self, app, options):
"""Register the blueprint to the sanic app.""" """Register the blueprint to the sanic app."""

View File

@ -182,7 +182,7 @@ class Request(dict):
@property @property
def socket(self): def socket(self):
if not hasattr(self, '_socket'): if not hasattr(self, '_socket'):
self._get_socket() self._get_address()
return self._socket return self._socket
def _get_address(self): def _get_address(self):

View File

@ -234,11 +234,11 @@ class Router:
if properties['unhashable']: if properties['unhashable']:
routes_to_check = self.routes_always_check routes_to_check = self.routes_always_check
ndx, route = self.check_dynamic_route_exists( ndx, route = self.check_dynamic_route_exists(
pattern, routes_to_check) pattern, routes_to_check, parameters)
else: else:
routes_to_check = self.routes_dynamic[url_hash(uri)] routes_to_check = self.routes_dynamic[url_hash(uri)]
ndx, route = self.check_dynamic_route_exists( ndx, route = self.check_dynamic_route_exists(
pattern, routes_to_check) pattern, routes_to_check, parameters)
if ndx != -1: if ndx != -1:
# Pop the ndx of the route, no dups of the same route # Pop the ndx of the route, no dups of the same route
routes_to_check.pop(ndx) routes_to_check.pop(ndx)
@ -285,9 +285,9 @@ class Router:
self.routes_static[uri] = route self.routes_static[uri] = route
@staticmethod @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): for ndx, route in enumerate(routes_to_check):
if route.pattern == pattern: if route.pattern == pattern and route.parameters == parameters:
return ndx, route return ndx, route
else: else:
return -1, None return -1, None

View File

@ -5,7 +5,7 @@ from functools import partial
from inspect import isawaitable from inspect import isawaitable
from multiprocessing import Process from multiprocessing import Process
from signal import ( from signal import (
SIGTERM, SIGINT, SIGTERM, SIGINT, SIG_IGN,
signal as signal_func, signal as signal_func,
Signals Signals
) )
@ -20,9 +20,10 @@ from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError from httptools.parser.errors import HttpParserError
try: try:
import uvloop as async_loop import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError: except ImportError:
async_loop = asyncio pass
from sanic.log import logger, access_logger from sanic.log import logger, access_logger
from sanic.response import HTTPResponse 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, request_timeout=60, response_timeout=60, keep_alive_timeout=5,
ssl=None, sock=None, request_max_size=None, reuse_port=False, ssl=None, sock=None, request_max_size=None, reuse_port=False,
loop=None, protocol=HttpProtocol, backlog=100, loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True, run_async=False, connections=None, register_sys_signals=True, run_multiple=False, run_async=False,
signal=Signal(), request_class=None, access_log=True, connections=None, signal=Signal(), request_class=None,
keep_alive=True, is_request_stream=False, router=None, access_log=True, keep_alive=True, is_request_stream=False,
websocket_max_size=None, websocket_max_queue=None, state=None, router=None, websocket_max_size=None, websocket_max_queue=None,
graceful_shutdown_timeout=15.0): state=None, graceful_shutdown_timeout=15.0):
"""Start asynchronous HTTP Server on an individual process. """Start asynchronous HTTP Server on an individual process.
:param host: Address to host on :param host: Address to host on
@ -547,7 +548,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
:return: Nothing :return: Nothing
""" """
if not run_async: 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) asyncio.set_event_loop(loop)
if debug: if debug:
@ -603,9 +605,14 @@ def serve(host, port, request_handler, error_handler, before_start=None,
trigger_events(after_start, loop) trigger_events(after_start, loop)
# Ignore SIGINT when run_multiple
if run_multiple:
signal_func(SIGINT, SIG_IGN)
# Register signals for graceful termination # Register signals for graceful termination
if register_sys_signals: if register_sys_signals:
for _signal in (SIGINT, SIGTERM): _singals = (SIGTERM,) if run_multiple else (SIGINT, SIGTERM)
for _signal in _singals:
try: try:
loop.add_signal_handler(_signal, loop.stop) loop.add_signal_handler(_signal, loop.stop)
except NotImplementedError: except NotImplementedError:
@ -668,6 +675,7 @@ def serve_multiple(server_settings, workers):
:return: :return:
""" """
server_settings['reuse_port'] = True server_settings['reuse_port'] = True
server_settings['run_multiple'] = True
# Handling when custom socket is not provided. # Handling when custom socket is not provided.
if server_settings.get('sock') is None: if server_settings.get('sock') is None:
@ -682,12 +690,13 @@ def serve_multiple(server_settings, workers):
def sig_handler(signal, frame): def sig_handler(signal, frame):
logger.info("Received signal %s. Shutting down.", Signals(signal).name) logger.info("Received signal %s. Shutting down.", Signals(signal).name)
for process in processes: 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(SIGINT, lambda s, f: sig_handler(s, f))
signal_func(SIGTERM, lambda s, f: sig_handler(s, f)) signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
processes = [] processes = []
for _ in range(workers): for _ in range(workers):
process = Process(target=serve, kwargs=server_settings) process = Process(target=serve, kwargs=server_settings)
process.daemon = True process.daemon = True

View File

@ -446,3 +446,44 @@ def test_bp_shorthand():
'Sec-WebSocket-Version': '13'}) 'Sec-WebSocket-Version': '13'})
assert response.status == 101 assert response.status == 101
assert ev.is_set() 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'

View File

@ -23,4 +23,3 @@ def test_multiprocessing():
app.run(HOST, app.test_port, workers=num_workers) app.run(HOST, app.test_port, workers=num_workers)
assert len(process_list) == num_workers assert len(process_list) == num_workers

View File

@ -2,7 +2,7 @@ import asyncio
import pytest import pytest
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text, json
from sanic.router import RouteExists, RouteDoesNotExist from sanic.router import RouteExists, RouteDoesNotExist
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
@ -907,3 +907,27 @@ def test_unicode_routes():
request, response = app.test_client.get('/overload/你好') request, response = app.test_client.get('/overload/你好')
assert response.text == 'OK2 你好' 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'
}