From b5e50ecb75afab6ffdffe0deab486b6c91cd37c6 Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Sat, 11 Feb 2017 14:30:17 +0000 Subject: [PATCH 01/14] Use app decorator instead of run arguments for before_start Mirror listener of blueprints --- docs/sanic/deploying.md | 9 --- examples/limit_concurrency.py | 3 +- examples/sanic_aiopg_example.py | 4 +- examples/sanic_aiopg_sqlalchemy_example.py | 7 +- examples/sanic_asyncpg_example.py | 3 +- examples/sanic_peewee.py | 19 ++--- examples/try_everything.py | 4 +- sanic/blueprints.py | 5 ++ sanic/sanic.py | 83 ++++++++++------------ sanic/utils.py | 5 +- tests/test_bad_request.py | 3 +- tests/test_server_events.py | 15 ++-- tests/test_signal_handlers.py | 19 ++--- 13 files changed, 90 insertions(+), 89 deletions(-) diff --git a/docs/sanic/deploying.md b/docs/sanic/deploying.md index 0c526efe..d5f3ad06 100644 --- a/docs/sanic/deploying.md +++ b/docs/sanic/deploying.md @@ -7,15 +7,6 @@ keyword arguments: - `host` *(default `"127.0.0.1"`)*: Address to host the server on. - `port` *(default `8000`)*: Port to host the server on. - `debug` *(default `False`)*: Enables debug output (slows server). -- `before_start` *(default `None`)*: Function or list of functions to be executed - before the server starts accepting connections. -- `after_start` *(default `None`)*: Function or list of functions to be executed - after the server starts accepting connections. -- `before_stop` *(default `None`)*: Function or list of functions to be - executed when a stop signal is received before it is - respected. -- `after_stop` *(default `None`)*: Function or list of functions to be executed - when all requests are complete. - `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s). - `sock` *(default `None`)*: Socket for the server to accept connections from. - `workers` *(default `1`)*: Number of worker processes to spawn. diff --git a/examples/limit_concurrency.py b/examples/limit_concurrency.py index 16307462..8d38a813 100644 --- a/examples/limit_concurrency.py +++ b/examples/limit_concurrency.py @@ -8,6 +8,7 @@ app = Sanic(__name__) sem = None +@app.listener('before_server_start') def init(sanic, loop): global sem CONCURRENCY_PER_WORKER = 4 @@ -33,4 +34,4 @@ async def test(request): return json(response) -app.run(host="0.0.0.0", port=8000, workers=2, before_start=init) +app.run(host="0.0.0.0", port=8000, workers=2) diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py index fb1ea100..16bde35f 100644 --- a/examples/sanic_aiopg_example.py +++ b/examples/sanic_aiopg_example.py @@ -26,6 +26,7 @@ async def get_pool(): app = Sanic(name=__name__) +@app.listener('before_server_start') async def prepare_db(app, loop): """ Let's create some table and add some data @@ -61,5 +62,4 @@ async def handle(request): if __name__ == '__main__': app.run(host='0.0.0.0', port=8000, - debug=True, - before_start=prepare_db) + debug=True) diff --git a/examples/sanic_aiopg_sqlalchemy_example.py b/examples/sanic_aiopg_sqlalchemy_example.py index 802d4fbe..34bda28a 100644 --- a/examples/sanic_aiopg_sqlalchemy_example.py +++ b/examples/sanic_aiopg_sqlalchemy_example.py @@ -32,7 +32,7 @@ polls = sa.Table('sanic_polls', metadata, app = Sanic(name=__name__) - +@app.listener('before_server_start') async def prepare_db(app, loop): """ Let's add some data @@ -58,9 +58,10 @@ async def handle(request): async with engine.acquire() as conn: result = [] async for row in conn.execute(polls.select()): - result.append({"question": row.question, "pub_date": row.pub_date}) + result.append({"question": row.question, + "pub_date": row.pub_date}) return json({"polls": result}) if __name__ == '__main__': - app.run(host='0.0.0.0', port=8000, before_start=prepare_db) + app.run(host='0.0.0.0', port=8000) diff --git a/examples/sanic_asyncpg_example.py b/examples/sanic_asyncpg_example.py index 9bb7d9c1..633fc50a 100644 --- a/examples/sanic_asyncpg_example.py +++ b/examples/sanic_asyncpg_example.py @@ -27,6 +27,7 @@ def jsonify(records): app = Sanic(__name__) +@app.listener('before_server_start') async def create_db(app, loop): """ Create some table and add some data @@ -55,4 +56,4 @@ async def handler(request): if __name__ == '__main__': - app.run(host='0.0.0.0', port=8000, before_start=create_db) + app.run(host='0.0.0.0', port=8000) diff --git a/examples/sanic_peewee.py b/examples/sanic_peewee.py index f3981b36..8db8ddff 100644 --- a/examples/sanic_peewee.py +++ b/examples/sanic_peewee.py @@ -14,14 +14,6 @@ from peewee_async import Manager, PostgresqlDatabase # we instantiate a custom loop so we can pass it to our db manager -def setup(app, loop): - database = PostgresqlDatabase(database='test', - host='127.0.0.1', - user='postgres', - password='mysecretpassword') - - objects = Manager(database, loop=loop) - ## from peewee_async docs: # Also there’s no need to connect and re-connect before executing async queries # with manager! It’s all automatic. But you can run Manager.connect() or @@ -48,6 +40,15 @@ objects.database.allow_sync = False # this will raise AssertionError on ANY sync app = Sanic('peewee_example') +@app.listener('before_server_start') +def setup(app, loop): + database = PostgresqlDatabase(database='test', + host='127.0.0.1', + user='postgres', + password='mysecretpassword') + + objects = Manager(database, loop=loop) + @app.route('/post//') async def post(request, key, value): """ @@ -75,4 +76,4 @@ async def get(request): if __name__ == "__main__": - app.run(host='0.0.0.0', port=8000, before_start=setup) + app.run(host='0.0.0.0', port=8000) diff --git a/examples/try_everything.py b/examples/try_everything.py index f386fb03..e94ffc6e 100644 --- a/examples/try_everything.py +++ b/examples/try_everything.py @@ -64,12 +64,14 @@ def query_string(request): # Run Server # ----------------------------------------------- # +@app.listener('after_server_start') def after_start(app, loop): log.info("OH OH OH OH OHHHHHHHH") +@app.listener('before_server_stop') def before_stop(app, loop): log.info("TRIED EVERYTHING") -app.run(host="0.0.0.0", port=8000, debug=True, after_start=after_start, before_stop=before_stop) +app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 0c14f4bc..64a37da0 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -65,6 +65,11 @@ class Blueprint: app.static(uri, future.file_or_directory, *future.args, **future.kwargs) + # Event listeners + for event, listeners in self.listeners.items(): + for listener in listeners: + app.listener(event)(listener) + def route(self, uri, methods=frozenset({'GET'}), host=None): """ Creates a blueprint route from a decorated function. diff --git a/sanic/sanic.py b/sanic/sanic.py index 951632be..1d7233bf 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -2,7 +2,7 @@ import logging import re import warnings from asyncio import get_event_loop -from collections import deque +from collections import deque, defaultdict from functools import partial from inspect import isawaitable, stack, getmodulename from traceback import format_exc @@ -10,8 +10,8 @@ from urllib.parse import urlencode, urlunparse from .config import Config from .constants import HTTP_METHODS -from .handlers import ErrorHandler from .exceptions import ServerError, URLBuildError +from .handlers import ErrorHandler from .log import log from .response import HTTPResponse from .router import Router @@ -20,6 +20,7 @@ from .static import register as static_register class Sanic: + def __init__(self, name=None, router=None, error_handler=None): # Only set up a default log handler if the @@ -45,6 +46,7 @@ class Sanic: self.debug = None self.sock = None self.processes = None + self.listeners = defaultdict(list) # Register alternative method names self.go_fast = self.run @@ -53,6 +55,18 @@ class Sanic: # Registration # -------------------------------------------------------------------- # + # Decorator + def listener(self, event): + """ + Create a listener from a decorated function. + + :param event: Event to listen to. + """ + def decorator(listener): + self.listeners[event].append(listener) + return listener + return decorator + # Decorator def route(self, uri, methods=frozenset({'GET'}), host=None): """ @@ -367,8 +381,7 @@ class Sanic: # Execution # -------------------------------------------------------------------- # - def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, - after_start=None, before_stop=None, after_stop=None, ssl=None, + def run(self, host="127.0.0.1", port=8000, debug=False, ssl=None, sock=None, workers=1, loop=None, protocol=HttpProtocol, backlog=100, stop_event=None, register_sys_signals=True): """ @@ -378,14 +391,6 @@ class Sanic: :param host: Address to host on :param port: Port to host on :param debug: Enables debug output (slows server) - :param before_start: Functions to be executed before the server starts - accepting connections - :param after_start: Functions to be executed after the server starts - accepting connections - :param before_stop: Functions to be executed when a stop signal is - received before it is respected - :param after_stop: Functions to be executed when all requests are - complete :param ssl: SSLContext for SSL encryption of worker(s) :param sock: Socket for the server to accept connections from :param workers: Number of processes @@ -398,11 +403,11 @@ class Sanic: :return: Nothing """ server_settings = self._helper( - host=host, port=port, debug=debug, before_start=before_start, - after_start=after_start, before_stop=before_stop, - after_stop=after_stop, ssl=ssl, sock=sock, workers=workers, - loop=loop, protocol=protocol, backlog=backlog, - stop_event=stop_event, register_sys_signals=register_sys_signals) + host=host, port=port, debug=debug, ssl=ssl, sock=sock, + workers=workers, loop=loop, protocol=protocol, backlog=backlog, + stop_event=stop_event, register_sys_signals=register_sys_signals + ) + try: if workers == 1: serve(**server_settings) @@ -418,19 +423,16 @@ class Sanic: get_event_loop().stop() async def create_server(self, host="127.0.0.1", port=8000, debug=False, - before_start=None, after_start=None, - before_stop=None, after_stop=None, ssl=None, - sock=None, loop=None, protocol=HttpProtocol, - backlog=100, stop_event=None): + ssl=None, sock=None, loop=None, + protocol=HttpProtocol, backlog=100, + stop_event=None): """ Asynchronous version of `run`. """ - server_settings = self._helper( - host=host, port=port, debug=debug, before_start=before_start, - after_start=after_start, before_stop=before_stop, - after_stop=after_stop, ssl=ssl, sock=sock, loop=loop, - protocol=protocol, backlog=backlog, stop_event=stop_event, - run_async=True) + server_settings = self._helper(host=host, port=port, debug=debug, + ssl=ssl, sock=sock, loop=loop, + protocol=protocol, backlog=backlog, + stop_event=stop_event, run_async=True) # Serve proto = "http" @@ -440,11 +442,10 @@ class Sanic: return await serve(**server_settings) - def _helper(self, host="127.0.0.1", port=8000, debug=False, - before_start=None, after_start=None, before_stop=None, - after_stop=None, ssl=None, sock=None, workers=1, loop=None, - protocol=HttpProtocol, backlog=100, stop_event=None, - register_sys_signals=True, run_async=False): + def _helper(self, host="127.0.0.1", port=8000, debug=False, ssl=None, + sock=None, workers=1, loop=None, protocol=HttpProtocol, + backlog=100, stop_event=None, register_sys_signals=True, + run_async=False): """ Helper function used by `run` and `create_server`. """ @@ -481,19 +482,13 @@ class Sanic: # Register start/stop events # -------------------------------------------- # - for event_name, settings_name, args, reverse in ( - ("before_server_start", "before_start", before_start, False), - ("after_server_start", "after_start", after_start, False), - ("before_server_stop", "before_stop", before_stop, True), - ("after_server_stop", "after_stop", after_stop, True), + for event_name, settings_name, reverse in ( + ("before_server_start", "before_start", False), + ("after_server_start", "after_start", False), + ("before_server_stop", "before_stop", True), + ("after_server_stop", "after_stop", True), ): - listeners = [] - for blueprint in self.blueprints.values(): - listeners += blueprint.listeners[event_name] - if args: - if callable(args): - args = [args] - listeners += args + listeners = self.listeners[event_name].copy() if reverse: listeners.reverse() # Prepend sanic to the arguments when listeners are triggered diff --git a/sanic/utils.py b/sanic/utils.py index 8e8f8124..c7520151 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -28,6 +28,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, results[0] = request app.request_middleware.appendleft(_collect_request) + @app.listener('after_server_start') # TODO undo this async def _collect_response(sanic, loop): try: response = await local_request(method, uri, *request_args, @@ -37,8 +38,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, exceptions.append(e) app.stop() - app.run(host=HOST, debug=debug, port=PORT, - after_start=_collect_response, **server_kwargs) + app.run(host=HOST, debug=debug, port=PORT, **server_kwargs) + app.listeners['after_server_start'].pop() if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) diff --git a/tests/test_bad_request.py b/tests/test_bad_request.py index 095f4ab1..bf595085 100644 --- a/tests/test_bad_request.py +++ b/tests/test_bad_request.py @@ -5,6 +5,7 @@ from sanic import Sanic def test_bad_request_response(): app = Sanic('test_bad_request_response') lines = [] + @app.listener('after_server_start') async def _request(sanic, loop): connect = asyncio.open_connection('127.0.0.1', 42101) reader, writer = await connect @@ -15,6 +16,6 @@ def test_bad_request_response(): break lines.append(line) app.stop() - app.run(host='127.0.0.1', port=42101, debug=False, after_start=_request) + app.run(host='127.0.0.1', port=42101, debug=False) assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n' assert lines[-1] == b'Error: Bad Request' diff --git a/tests/test_server_events.py b/tests/test_server_events.py index fce98440..05484b8c 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -42,9 +42,10 @@ def test_single_listener(listener_name): random_name_app = Sanic(''.join( [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) output = list() - start_stop_app( - random_name_app, - **{listener_name: create_listener(listener_name, output)}) + # Register listener + random_name_app.listener(listener_name)( + create_listener(listener_name, output)) + start_stop_app(random_name_app) assert random_name_app.name + listener_name == output.pop() @@ -52,9 +53,9 @@ def test_all_listeners(): random_name_app = Sanic(''.join( [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) output = list() - start_stop_app( - random_name_app, - **{listener_name: create_listener(listener_name, output) - for listener_name in AVAILABLE_LISTENERS}) + for listener_name in AVAILABLE_LISTENERS: + listener = create_listener(listener_name, output) + random_name_app.listener(listener_name)(listener) + start_stop_app(random_name_app) for listener_name in AVAILABLE_LISTENERS: assert random_name_app.name + listener_name == output.pop() diff --git a/tests/test_signal_handlers.py b/tests/test_signal_handlers.py index 756df828..3654e57c 100644 --- a/tests/test_signal_handlers.py +++ b/tests/test_signal_handlers.py @@ -27,10 +27,11 @@ def test_register_system_signals(): async def hello_route(request): return HTTPResponse() - app.run(HOST, PORT, - before_start=set_loop, - after_start=stop, - after_stop=after) + app.listener('after_server_start')(stop) + app.listener('before_server_start')(set_loop) + app.listener('after_server_stop')(after) + + app.run(HOST, PORT) assert calledq.get() == True @@ -42,9 +43,9 @@ def test_dont_register_system_signals(): async def hello_route(request): return HTTPResponse() - app.run(HOST, PORT, - before_start=set_loop, - after_start=stop, - after_stop=after, - register_sys_signals=False) + app.listener('after_server_start')(stop) + app.listener('before_server_start')(set_loop) + app.listener('after_server_stop')(after) + + app.run(HOST, PORT, register_sys_signals=False) assert calledq.get() == False From 2d5fd2fe1c763c48ec41290c8656b83003397b99 Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Sat, 11 Feb 2017 14:35:44 +0000 Subject: [PATCH 02/14] fix test --- tests/test_server_events.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_server_events.py b/tests/test_server_events.py index 05484b8c..3335489e 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -9,10 +9,10 @@ from sanic import Sanic from sanic.utils import HOST, PORT AVAILABLE_LISTENERS = [ - 'before_start', - 'after_start', - 'before_stop', - 'after_stop' + 'before_server_start', + 'after_server_start', + 'before_server_stop', + 'after_server_stop' ] From 8b08a370c54f490b04d45d129e901e4d58fb9d89 Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Sat, 11 Feb 2017 14:39:32 +0000 Subject: [PATCH 03/14] Remove todo --- sanic/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/utils.py b/sanic/utils.py index c7520151..272ec8a8 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -28,7 +28,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, results[0] = request app.request_middleware.appendleft(_collect_request) - @app.listener('after_server_start') # TODO undo this + @app.listener('after_server_start') async def _collect_response(sanic, loop): try: response = await local_request(method, uri, *request_args, From 94b2352c2c60a628b5fbc7e7ad0f0431f71b5dfa Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 11 Feb 2017 14:52:40 -0800 Subject: [PATCH 04/14] add ensure_future method --- sanic/sanic.py | 28 ++++++++++++++++++++++++++-- tests/test_ensure_future.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 tests/test_ensure_future.py diff --git a/sanic/sanic.py b/sanic/sanic.py index 951632be..218937c5 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -1,7 +1,7 @@ import logging import re import warnings -from asyncio import get_event_loop +from asyncio import get_event_loop, ensure_future from collections import deque from functools import partial from inspect import isawaitable, stack, getmodulename @@ -44,7 +44,7 @@ class Sanic: self._blueprint_order = [] self.debug = None self.sock = None - self.processes = None + self.before_start = [] # Register alternative method names self.go_fast = self.run @@ -53,6 +53,23 @@ class Sanic: # Registration # -------------------------------------------------------------------- # + def ensure_future(self, task): + """ + Schedule a task to run later, after the loop has started. + Different from asyncio.ensure_future in that it does not + also return a future, and the actual ensure_future call + is delayed until before server start. + + :param task: A future, couroutine or awaitable. + """ + def run(app, loop): + if callable(task): + ensure_future(task()) + else: + ensure_future(task) + + self.before_start.append(run) + # Decorator def route(self, uri, methods=frozenset({'GET'}), host=None): """ @@ -397,6 +414,13 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ + if before_start is not None: + if not isinstance(before_start, list): + before_start = [before_start] + before_start.extend(self.before_start) + else: + before_start = self.before_start + server_settings = self._helper( host=host, port=port, debug=debug, before_start=before_start, after_start=after_start, before_stop=before_stop, diff --git a/tests/test_ensure_future.py b/tests/test_ensure_future.py new file mode 100644 index 00000000..385d8b88 --- /dev/null +++ b/tests/test_ensure_future.py @@ -0,0 +1,30 @@ +import sanic +from sanic.utils import sanic_endpoint_test +from sanic.response import text +from threading import Event +import asyncio + +def test_ensure_future(): + e = Event() + async def coro(): + await asyncio.sleep(0.05) + e.set() + + app = sanic.Sanic() + app.ensure_future(coro) + + @app.route('/early') + def not_set(request): + return text(e.is_set()) + + @app.route('/late') + async def set(request): + await asyncio.sleep(0.1) + return text(e.is_set()) + + + request, response = sanic_endpoint_test(app, uri='/early') + assert response.body == b'False' + + request, response = sanic_endpoint_test(app, uri='/late') + assert response.body == b'True' From ee6ff0cc60e838488db8bd8b7d37f0c75d62f2cf Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Sun, 12 Feb 2017 12:28:02 +0000 Subject: [PATCH 05/14] Add deprecation and old API --- sanic/sanic.py | 68 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 1d7233bf..dec6d10b 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -381,7 +381,8 @@ class Sanic: # Execution # -------------------------------------------------------------------- # - def run(self, host="127.0.0.1", port=8000, debug=False, ssl=None, + def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, + after_start=None, before_stop=None, after_stop=None, ssl=None, sock=None, workers=1, loop=None, protocol=HttpProtocol, backlog=100, stop_event=None, register_sys_signals=True): """ @@ -391,6 +392,14 @@ class Sanic: :param host: Address to host on :param port: Port to host on :param debug: Enables debug output (slows server) + :param before_start: Functions to be executed before the server starts + accepting connections + :param after_start: Functions to be executed after the server starts + accepting connections + :param before_stop: Functions to be executed when a stop signal is + received before it is respected + :param after_stop: Functions to be executed when all requests are + complete :param ssl: SSLContext for SSL encryption of worker(s) :param sock: Socket for the server to accept connections from :param workers: Number of processes @@ -403,10 +412,11 @@ class Sanic: :return: Nothing """ server_settings = self._helper( - host=host, port=port, debug=debug, ssl=ssl, sock=sock, - workers=workers, loop=loop, protocol=protocol, backlog=backlog, - stop_event=stop_event, register_sys_signals=register_sys_signals - ) + host=host, port=port, debug=debug, before_start=before_start, + after_start=after_start, before_stop=before_stop, + after_stop=after_stop, ssl=ssl, sock=sock, workers=workers, + loop=loop, protocol=protocol, backlog=backlog, + stop_event=stop_event, register_sys_signals=register_sys_signals) try: if workers == 1: @@ -423,16 +433,19 @@ class Sanic: get_event_loop().stop() async def create_server(self, host="127.0.0.1", port=8000, debug=False, - ssl=None, sock=None, loop=None, - protocol=HttpProtocol, backlog=100, - stop_event=None): + before_start=None, after_start=None, + before_stop=None, after_stop=None, ssl=None, + sock=None, loop=None, protocol=HttpProtocol, + backlog=100, stop_event=None): """ Asynchronous version of `run`. """ - server_settings = self._helper(host=host, port=port, debug=debug, - ssl=ssl, sock=sock, loop=loop, - protocol=protocol, backlog=backlog, - stop_event=stop_event, run_async=True) + server_settings = self._helper( + host=host, port=port, debug=debug, before_start=before_start, + after_start=after_start, before_stop=before_stop, + after_stop=after_stop, ssl=ssl, sock=sock, loop=loop, + protocol=protocol, backlog=backlog, stop_event=stop_event, + run_async=True) # Serve proto = "http" @@ -442,10 +455,11 @@ class Sanic: return await serve(**server_settings) - def _helper(self, host="127.0.0.1", port=8000, debug=False, ssl=None, - sock=None, workers=1, loop=None, protocol=HttpProtocol, - backlog=100, stop_event=None, register_sys_signals=True, - run_async=False): + def _helper(self, host="127.0.0.1", port=8000, debug=False, + before_start=None, after_start=None, before_stop=None, + after_stop=None, ssl=None, sock=None, workers=1, loop=None, + protocol=HttpProtocol, backlog=100, stop_event=None, + register_sys_signals=True, run_async=False): """ Helper function used by `run` and `create_server`. """ @@ -458,6 +472,16 @@ class Sanic: "pull/335 has more information.", DeprecationWarning) + # Deprecate this + if any(arg is not None for arg in (after_stop, after_start, + before_start, before_stop)): + if debug: + warnings.simplefilter('default') + warnings.warn("Passing a before_start, before_stop, after_start or" + "after_stop callback will be deprecated in version" + " 0.4.0", + DeprecationWarning) + self.error_handler.debug = debug self.debug = debug self.loop = loop = get_event_loop() @@ -482,13 +506,15 @@ class Sanic: # Register start/stop events # -------------------------------------------- # - for event_name, settings_name, reverse in ( - ("before_server_start", "before_start", False), - ("after_server_start", "after_start", False), - ("before_server_stop", "before_stop", True), - ("after_server_stop", "after_stop", True), + for event_name, settings_name, reverse, args in ( + ("before_server_start", "before_start", False, before_start), + ("after_server_start", "after_start", False, after_start), + ("before_server_stop", "before_stop", True, before_stop), + ("after_server_stop", "after_stop", True, after_stop), ): listeners = self.listeners[event_name].copy() + if args: + listeners.extend(args) if reverse: listeners.reverse() # Prepend sanic to the arguments when listeners are triggered From 2340910b4632ca48f5c968907226e6c1095b2e2b Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Sun, 12 Feb 2017 18:15:14 +0000 Subject: [PATCH 06/14] Update deprecation message Fix bug with single callbacks --- sanic/sanic.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index dec6d10b..1f2a6921 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -478,8 +478,8 @@ class Sanic: if debug: warnings.simplefilter('default') warnings.warn("Passing a before_start, before_stop, after_start or" - "after_stop callback will be deprecated in version" - " 0.4.0", + "after_stop callback will be deprecated in next " + "major version after 0.4.0", DeprecationWarning) self.error_handler.debug = debug @@ -514,7 +514,10 @@ class Sanic: ): listeners = self.listeners[event_name].copy() if args: - listeners.extend(args) + if callable(args): + listeners.append(args) + else: + listeners.extend(args) if reverse: listeners.reverse() # Prepend sanic to the arguments when listeners are triggered From b5e46e83e27ccfd63e864522fdff9054d6f76c9a Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 12 Feb 2017 12:07:59 -0800 Subject: [PATCH 07/14] ensure_future -> add_task --- sanic/sanic.py | 8 ++++---- tests/{test_ensure_future.py => test_create_task.py} | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename tests/{test_ensure_future.py => test_create_task.py} (96%) diff --git a/sanic/sanic.py b/sanic/sanic.py index 218937c5..fa2f298d 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -1,7 +1,7 @@ import logging import re import warnings -from asyncio import get_event_loop, ensure_future +from asyncio import get_event_loop from collections import deque from functools import partial from inspect import isawaitable, stack, getmodulename @@ -53,7 +53,7 @@ class Sanic: # Registration # -------------------------------------------------------------------- # - def ensure_future(self, task): + def add_task(self, task): """ Schedule a task to run later, after the loop has started. Different from asyncio.ensure_future in that it does not @@ -64,9 +64,9 @@ class Sanic: """ def run(app, loop): if callable(task): - ensure_future(task()) + loop.create_task(task()) else: - ensure_future(task) + loop.create_task(task) self.before_start.append(run) diff --git a/tests/test_ensure_future.py b/tests/test_create_task.py similarity index 96% rename from tests/test_ensure_future.py rename to tests/test_create_task.py index 385d8b88..76efff36 100644 --- a/tests/test_ensure_future.py +++ b/tests/test_create_task.py @@ -11,7 +11,7 @@ def test_ensure_future(): e.set() app = sanic.Sanic() - app.ensure_future(coro) + app.add_task(coro) @app.route('/early') def not_set(request): From 41c6125e1b5414b6d784067681739a818e82e579 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 12 Feb 2017 14:40:10 -0800 Subject: [PATCH 08/14] use try/except when adding loop sig handlers --- sanic/server.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index 8b9ed532..8a7969e2 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -346,7 +346,11 @@ def serve(host, port, request_handler, error_handler, before_start=None, # Register signals for graceful termination if register_sys_signals: for _signal in (SIGINT, SIGTERM): - loop.add_signal_handler(_signal, loop.stop) + try: + loop.add_signal_handler(_signal, loop.stop) + except NotImplementedError: + log.warn(('Sanic tried to use loop.add_signal_handler') + ('but it is not implemented on this platform.')) pid = os.getpid() try: From d614823013b38ae2026c7a7937ab0667c5dddfaf Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Thu, 2 Feb 2017 16:24:16 -0500 Subject: [PATCH 09/14] rebase --- sanic/router.py | 38 +++++++++++++++++++++++++----- sanic/sanic.py | 12 +++++++++- sanic/views.py | 21 ++++++++--------- tests/test_views.py | 56 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 107 insertions(+), 20 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index 24d0438f..7ae28f09 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -4,8 +4,7 @@ from functools import lru_cache from .exceptions import NotFound, InvalidUsage from .views import CompositionView -Route = namedtuple( - 'Route', +Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters', 'name']) Parameter = namedtuple('Parameter', ['name', 'cast']) @@ -70,6 +69,28 @@ class Router: self.routes_always_check = [] self.hosts = None + def __str__(self): + """ + The typical user inspecting the router will likely want to see + the routes available. Provide a simple representation. + """ + def _route_to_str(uri, route): + out = 'name={0.name}, methods={0.methods}, URI={1}>\n'.format( + route, uri) + + if route.handler.__doc__: + out += '{}\n'.format(route.handler.__doc__) + + out += '\n' + + return out + + out = '' + for uri, route in self.routes_all.items(): + out += _route_to_str(uri, route) + + return out + def parse_parameter_string(self, parameter_string): """ Parse a parameter string into its constituent name, type, and pattern @@ -130,11 +151,16 @@ class Router: properties = {"unhashable": None} def add_parameter(match): + # We could receive NAME or NAME:PATTERN name = match.group(1) - name, _type, pattern = self.parse_parameter_string(name) + pattern = 'string' + if ':' in name: + name, pattern = name.split(':', 1) - parameter = Parameter( - name=name, cast=_type) + default = (str, pattern) + # Pull from pre-configured types + _type, pattern = REGEX_TYPES.get(pattern, default) + parameter = Parameter(name=name, cast=_type) parameters.append(parameter) # Mark the whole route as unhashable if it has the hash key in it @@ -146,7 +172,7 @@ class Router: return '({})'.format(pattern) - pattern_string = re.sub(self.parameter_pattern, add_parameter, uri) + pattern_string = re.sub(r'<(.+?)>', add_parameter, uri) pattern = re.compile(r'^{}$'.format(pattern_string)) def merge_route(route, methods, handler): diff --git a/sanic/sanic.py b/sanic/sanic.py index 4ad4734a..079049ca 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -17,6 +17,7 @@ from .response import HTTPResponse from .router import Router from .server import serve, serve_multiple, HttpProtocol from .static import register as static_register +from .views import CompositionView class Sanic: @@ -120,7 +121,16 @@ class Sanic: """ # Handle HTTPMethodView differently if hasattr(handler, 'view_class'): - methods = frozenset(HTTP_METHODS) + methods = set() + + for method in HTTP_METHODS: + if getattr(handler.view_class, method.lower(), None): + methods.add(method) + + # handle composition view differently + if isinstance(handler, CompositionView): + methods = handler.handlers.keys() + self.route(uri=uri, methods=methods, host=host)(handler) return handler diff --git a/sanic/views.py b/sanic/views.py index 5d8e9d40..d6da9145 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -1,4 +1,5 @@ from .exceptions import InvalidUsage +from .constants import HTTP_METHODS class HTTPMethodView: @@ -40,11 +41,7 @@ class HTTPMethodView: def dispatch_request(self, request, *args, **kwargs): handler = getattr(self, request.method.lower(), None) - if handler: - return handler(request, *args, **kwargs) - raise InvalidUsage( - 'Method {} not allowed for URL {}'.format( - request.method, request.url), status_code=405) + return handler(request, *args, **kwargs) @classmethod def as_view(cls, *class_args, **class_kwargs): @@ -89,15 +86,15 @@ class CompositionView: def add(self, methods, handler): for method in methods: + if method not in HTTP_METHODS: + raise InvalidUsage( + '{} is not a valid HTTP method.'.format(method)) + if method in self.handlers: - raise KeyError( - 'Method {} already is registered.'.format(method)) + raise InvalidUsage( + 'Method {} is already registered.'.format(method)) self.handlers[method] = handler def __call__(self, request, *args, **kwargs): - handler = self.handlers.get(request.method.upper(), None) - if handler is None: - raise InvalidUsage( - 'Method {} not allowed for URL {}'.format( - request.method, request.url), status_code=405) + handler = self.handlers[request.method.upper()] return handler(request, *args, **kwargs) diff --git a/tests/test_views.py b/tests/test_views.py index 4e5b17f0..456b714d 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,8 +1,9 @@ import pytest as pytest from sanic import Sanic +from sanic.exceptions import InvalidUsage from sanic.response import text, HTTPResponse -from sanic.views import HTTPMethodView +from sanic.views import HTTPMethodView, CompositionView from sanic.blueprints import Blueprint from sanic.request import Request from sanic.utils import sanic_endpoint_test @@ -196,3 +197,56 @@ def test_with_decorator(): request, response = sanic_endpoint_test(app, method="get") assert response.text == 'I am get method' assert results[0] == 1 + + +def test_composition_view_rejects_incorrect_methods(): + def foo(request): + return text('Foo') + + view = CompositionView() + + with pytest.raises(InvalidUsage) as e: + view.add(['GET', 'FOO'], foo) + + assert str(e.value) == 'FOO is not a valid HTTP method.' + + +def test_composition_view_rejects_duplicate_methods(): + def foo(request): + return text('Foo') + + view = CompositionView() + + with pytest.raises(InvalidUsage) as e: + view.add(['GET', 'POST', 'GET'], foo) + + assert str(e.value) == 'Method GET is already registered.' + + +def test_composition_view_runs_methods_as_expected(): + app = Sanic('test_composition_view') + + view = CompositionView() + view.add(['GET', 'POST', 'PUT'], lambda x: text('first method')) + view.add(['DELETE', 'PATCH'], lambda x: text('second method')) + + app.add_route(view, '/') + + for method in ['GET', 'POST', 'PUT']: + request, response = sanic_endpoint_test(app, uri='/', method=method) + assert response.text == 'first method' + + for method in ['DELETE', 'PATCH']: + request, response = sanic_endpoint_test(app, uri='/', method=method) + assert response.text == 'second method' + +def test_composition_view_rejects_invalid_methods(): + app = Sanic('test_composition_view') + + view = CompositionView() + view.add(['GET', 'POST', 'PUT'], lambda x: text('first method')) + + app.add_route(view, '/') + for method in ['DELETE', 'PATCH']: + request, response = sanic_endpoint_test(app, uri='/', method=method) + assert response.status == 405 From 4d6f9ffd7cfd4b6f929fdf2a16d661719497e14b Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Mon, 13 Feb 2017 11:45:55 -0500 Subject: [PATCH 10/14] rebase --- sanic/router.py | 11 +++-------- tests/test_views.py | 1 + 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index 7ae28f09..621d3733 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -151,16 +151,11 @@ class Router: properties = {"unhashable": None} def add_parameter(match): - # We could receive NAME or NAME:PATTERN name = match.group(1) - pattern = 'string' - if ':' in name: - name, pattern = name.split(':', 1) + name, _type, pattern = self.parse_parameter_string(name) - default = (str, pattern) - # Pull from pre-configured types - _type, pattern = REGEX_TYPES.get(pattern, default) - parameter = Parameter(name=name, cast=_type) + parameter = Parameter( + name=name, cast=_type) parameters.append(parameter) # Mark the whole route as unhashable if it has the hash key in it diff --git a/tests/test_views.py b/tests/test_views.py index 456b714d..afa52d8b 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -240,6 +240,7 @@ def test_composition_view_runs_methods_as_expected(): request, response = sanic_endpoint_test(app, uri='/', method=method) assert response.text == 'second method' + def test_composition_view_rejects_invalid_methods(): app = Sanic('test_composition_view') From 051ff2b325529a3d4953228df1d4de7d30236a23 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Mon, 13 Feb 2017 11:50:09 -0500 Subject: [PATCH 11/14] remove repr stuff --- sanic/router.py | 22 ---------------------- tests/test_views.py | 18 +++++++++++++----- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index 621d3733..c115b362 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -69,28 +69,6 @@ class Router: self.routes_always_check = [] self.hosts = None - def __str__(self): - """ - The typical user inspecting the router will likely want to see - the routes available. Provide a simple representation. - """ - def _route_to_str(uri, route): - out = 'name={0.name}, methods={0.methods}, URI={1}>\n'.format( - route, uri) - - if route.handler.__doc__: - out += '{}\n'.format(route.handler.__doc__) - - out += '\n' - - return out - - out = '' - for uri, route in self.routes_all.items(): - out += _route_to_str(uri, route) - - return out - def parse_parameter_string(self, parameter_string): """ Parse a parameter string into its constituent name, type, and pattern diff --git a/tests/test_views.py b/tests/test_views.py index afa52d8b..ec0b91b8 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -223,7 +223,8 @@ def test_composition_view_rejects_duplicate_methods(): assert str(e.value) == 'Method GET is already registered.' -def test_composition_view_runs_methods_as_expected(): +@pytest.mark.parametrize('method', HTTP_METHODS) +def test_composition_view_runs_methods_as_expected(method): app = Sanic('test_composition_view') view = CompositionView() @@ -232,22 +233,29 @@ def test_composition_view_runs_methods_as_expected(): app.add_route(view, '/') - for method in ['GET', 'POST', 'PUT']: + if method in ['GET', 'POST', 'PUT']: request, response = sanic_endpoint_test(app, uri='/', method=method) assert response.text == 'first method' - for method in ['DELETE', 'PATCH']: + if method in ['DELETE', 'PATCH']: request, response = sanic_endpoint_test(app, uri='/', method=method) assert response.text == 'second method' -def test_composition_view_rejects_invalid_methods(): +@pytest.mark.parametrize('method', HTTP_METHODS) +def test_composition_view_rejects_invalid_methods(method): app = Sanic('test_composition_view') view = CompositionView() view.add(['GET', 'POST', 'PUT'], lambda x: text('first method')) app.add_route(view, '/') - for method in ['DELETE', 'PATCH']: + + if method in ['GET', 'POST', 'PUT']: + request, response = sanic_endpoint_test(app, uri='/', method=method) + assert response.status == 200 + assert response.text == 'first method' + + if method in ['DELETE', 'PATCH']: request, response = sanic_endpoint_test(app, uri='/', method=method) assert response.status == 405 From b2be821637b18a6ab65ff67a1ce18ccf11d9367d Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Mon, 13 Feb 2017 11:55:00 -0500 Subject: [PATCH 12/14] reverse router changes --- sanic/router.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index c115b362..24d0438f 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -4,7 +4,8 @@ from functools import lru_cache from .exceptions import NotFound, InvalidUsage from .views import CompositionView -Route = namedtuple('Route', +Route = namedtuple( + 'Route', ['handler', 'methods', 'pattern', 'parameters', 'name']) Parameter = namedtuple('Parameter', ['name', 'cast']) @@ -145,7 +146,7 @@ class Router: return '({})'.format(pattern) - pattern_string = re.sub(r'<(.+?)>', add_parameter, uri) + pattern_string = re.sub(self.parameter_pattern, add_parameter, uri) pattern = re.compile(r'^{}$'.format(pattern_string)) def merge_route(route, methods, handler): From 758415d326c29b95f1707662797b9ce0b1c60cba Mon Sep 17 00:00:00 2001 From: argaen Date: Mon, 13 Feb 2017 23:08:42 +0100 Subject: [PATCH 13/14] Fixed aiocache example according to new loop policy --- examples/cache_example.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/cache_example.py b/examples/cache_example.py index 50a6fe85..7c8b2198 100644 --- a/examples/cache_example.py +++ b/examples/cache_example.py @@ -1,6 +1,6 @@ """ Example of caching using aiocache package. To run it you will need a Redis -instance running in localhost:6379. +instance running in localhost:6379. You can also try with SimpleMemoryCache. Running this example you will see that the first call lasts 3 seconds and the rest are instant because the value is retrieved from the Redis. @@ -20,9 +20,13 @@ from aiocache.serializers import JsonSerializer app = Sanic(__name__) -aiocache.settings.set_defaults( - class_="aiocache.RedisCache" -) + +def init_cache(sanic, loop): + aiocache.settings.set_defaults( + class_="aiocache.RedisCache", + # class_="aiocache.SimpleMemoryCache", + loop=loop + ) @cached(key="my_custom_key", serializer=JsonSerializer()) @@ -38,4 +42,4 @@ async def test(request): return json(await expensive_call()) -app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop()) +app.run(host="0.0.0.0", port=8000, before_start=init_cache) From b2af8e640cfd57831efe0defb2822434c2b0fdad Mon Sep 17 00:00:00 2001 From: argaen Date: Tue, 14 Feb 2017 00:12:39 +0100 Subject: [PATCH 14/14] Use decorator --- examples/cache_example.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/cache_example.py b/examples/cache_example.py index 7c8b2198..df56689f 100644 --- a/examples/cache_example.py +++ b/examples/cache_example.py @@ -21,6 +21,7 @@ from aiocache.serializers import JsonSerializer app = Sanic(__name__) +@app.listener('before_server_start') def init_cache(sanic, loop): aiocache.settings.set_defaults( class_="aiocache.RedisCache", @@ -42,4 +43,4 @@ async def test(request): return json(await expensive_call()) -app.run(host="0.0.0.0", port=8000, before_start=init_cache) +app.run(host="0.0.0.0", port=8000)