diff --git a/README.rst b/README.rst index 10bc8920..7f8ba439 100644 --- a/README.rst +++ b/README.rst @@ -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 ------------ diff --git a/docs/sanic/blueprints.md b/docs/sanic/blueprints.md index 1a7c5293..53aef5fd 100644 --- a/docs/sanic/blueprints.md +++ b/docs/sanic/blueprints.md @@ -51,6 +51,73 @@ will look like: [Route(handler=, 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. diff --git a/docs/sanic/request_data.md b/docs/sanic/request_data.md index 4f6bc970..a91dd970 100644 --- a/docs/sanic/request_data.md +++ b/docs/sanic/request_data.md @@ -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. diff --git a/sanic/app.py b/sanic/app.py index 26ef83b0..310d9059 100644 --- a/sanic/app.py +++ b/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 diff --git a/sanic/blueprints.py b/sanic/blueprints.py index f9159168..084013e1 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -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.""" diff --git a/sanic/request.py b/sanic/request.py index fcd4ed79..ecc41d13 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -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): diff --git a/sanic/router.py b/sanic/router.py index bd74c11d..052ff1bf 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -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 diff --git a/sanic/server.py b/sanic/server.py index e9790dae..2f2df444 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -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 diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 7e713da6..4c321646 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -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' diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 519ad171..cdbd91a8 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -23,4 +23,3 @@ def test_multiprocessing(): app.run(HOST, app.test_port, workers=num_workers) assert len(process_list) == num_workers - diff --git a/tests/test_routes.py b/tests/test_routes.py index 84d6b221..5f3b3376 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -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/', methods=['GET']) + async def ad_get(request, ad_id): + return json({'ad_id': ad_id}) + + @app.route('/ads/', 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' + }