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 079049ca..337c486a 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 @@ -21,6 +21,7 @@ from .views import CompositionView class Sanic: + def __init__(self, name=None, router=None, error_handler=None): # Only set up a default log handler if the @@ -46,6 +47,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 @@ -61,6 +63,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): """ @@ -419,6 +433,7 @@ class Sanic: 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: serve(**server_settings) @@ -473,6 +488,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 next " + "major version after 0.4.0", + DeprecationWarning) + self.error_handler.debug = debug self.debug = debug loop = self.loop @@ -497,19 +522,18 @@ 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, 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 = [] - for blueprint in self.blueprints.values(): - listeners += blueprint.listeners[event_name] + listeners = self.listeners[event_name].copy() if args: if callable(args): - args = [args] - listeners += args + listeners.append(args) + else: + listeners.extend(args) 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..272ec8a8 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') 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..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' ] @@ -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