diff --git a/sanic/mixins/listeners.py b/sanic/mixins/listeners.py index 160b51b5..e8effa13 100644 --- a/sanic/mixins/listeners.py +++ b/sanic/mixins/listeners.py @@ -1,8 +1,9 @@ from enum import Enum, auto from functools import partial -from typing import List, Optional, Union +from typing import Callable, List, Optional, Union, overload from sanic.base.meta import SanicMeta +from sanic.exceptions import InvalidUsage from sanic.models.futures import FutureListener from sanic.models.handler_types import ListenerType, Sanic @@ -28,12 +29,33 @@ class ListenerMixin(metaclass=SanicMeta): def _apply_listener(self, listener: FutureListener): raise NotImplementedError # noqa + @overload + def listener( + self, + listener_or_event: ListenerType[Sanic], + event_or_none: str, + apply: bool = ..., + ) -> ListenerType[Sanic]: + ... + + @overload + def listener( + self, + listener_or_event: str, + event_or_none: None = ..., + apply: bool = ..., + ) -> Callable[[ListenerType[Sanic]], ListenerType[Sanic]]: + ... + def listener( self, listener_or_event: Union[ListenerType[Sanic], str], event_or_none: Optional[str] = None, apply: bool = True, - ) -> ListenerType[Sanic]: + ) -> Union[ + ListenerType[Sanic], + Callable[[ListenerType[Sanic]], ListenerType[Sanic]], + ]: """ Create a listener from a decorated function. @@ -51,7 +73,9 @@ class ListenerMixin(metaclass=SanicMeta): :param event: event to listen to """ - def register_listener(listener, event): + def register_listener( + listener: ListenerType[Sanic], event: str + ) -> ListenerType[Sanic]: nonlocal apply future_listener = FutureListener(listener, event) @@ -61,6 +85,10 @@ class ListenerMixin(metaclass=SanicMeta): return listener if callable(listener_or_event): + if event_or_none is None: + raise InvalidUsage( + "Invalid event registration: Missing event name." + ) return register_listener(listener_or_event, event_or_none) else: return partial(register_listener, event=listener_or_event) diff --git a/sanic/models/handler_types.py b/sanic/models/handler_types.py index 317f0432..4a0fbfa6 100644 --- a/sanic/models/handler_types.py +++ b/sanic/models/handler_types.py @@ -1,11 +1,13 @@ from asyncio.events import AbstractEventLoop from typing import Any, Callable, Coroutine, Optional, TypeVar, Union +import sanic + from sanic.request import Request from sanic.response import BaseHTTPResponse, HTTPResponse -Sanic = TypeVar("Sanic") +Sanic = TypeVar("Sanic", bound="sanic.Sanic") MiddlewareResponse = Union[ Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]] diff --git a/tests/test_signal_handlers.py b/tests/test_signal_handlers.py index f7657ad6..c6838b5e 100644 --- a/tests/test_signal_handlers.py +++ b/tests/test_signal_handlers.py @@ -10,6 +10,7 @@ import pytest from sanic_testing.testing import HOST, PORT from sanic.compat import ctrlc_workaround_for_windows +from sanic.exceptions import InvalidUsage from sanic.response import HTTPResponse @@ -108,3 +109,17 @@ def test_windows_workaround(): assert res == "OK" res = loop.run_until_complete(atest(True)) assert res == "OK" + + +@pytest.mark.skipif(os.name == "nt", reason="May hang CI on py38/windows") +def test_signals_with_invalid_invocation(app): + """Test if sanic register fails with invalid invocation""" + + @app.route("/hello") + async def hello_route(request): + return HTTPResponse() + + with pytest.raises( + InvalidUsage, match="Invalid event registration: Missing event name" + ): + app.listener(stop)