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.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
------------

View File

@ -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.

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.
- `method` (str) - HTTP method of the request (ie `GET`, `POST`).
- `ip` (str) - IP address of the requester.
- `port` (str) - Port address of the requester.

View File

@ -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

View File

@ -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."""

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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'

View File

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

View File

@ -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'
}