From 781810514361797b405811d3b831007d1b64105f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ericson?= Date: Fri, 14 Jan 2022 22:26:25 +0100 Subject: [PATCH 1/2] Fixing typing for ListenerMixin.listener --- sanic/mixins/listeners.py | 31 ++++++++++++++++++++++++++++--- sanic/models/handler_types.py | 4 +++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/sanic/mixins/listeners.py b/sanic/mixins/listeners.py index cc8375f0d0..3d0569a8c1 100644 --- a/sanic/mixins/listeners.py +++ b/sanic/mixins/listeners.py @@ -1,6 +1,6 @@ from enum import Enum, auto from functools import partial -from typing import List, Optional, Union +from typing import TYPE_CHECKING, Callable, List, Optional, Union, overload from sanic.base.meta import SanicMeta from sanic.models.futures import FutureListener @@ -26,12 +26,33 @@ def __init__(self, *args, **kwargs) -> None: def _apply_listener(self, listener: FutureListener): raise NotImplementedError # noqa + @overload + def listener( + self, + listener_or_event: ListenerType[Sanic], + event_or_none: str, + apply: bool = True, + ) -> ListenerType[Sanic]: + ... + + @overload + def listener( + self, + listener_or_event: str, + event_or_none: None = None, + apply: bool = True, + ) -> 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. @@ -49,7 +70,9 @@ async def before_server_start(app, loop): :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) @@ -59,6 +82,8 @@ def register_listener(listener, event): return listener if callable(listener_or_event): + if TYPE_CHECKING: + assert event_or_none is not None 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 0144c964d8..986edea655 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]] From ee307e8f02b7e68034d1613796740b3d94187b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ericson?= Date: Sun, 16 Jan 2022 12:22:08 +0100 Subject: [PATCH 2/2] Use InvalidUsage instead of type assertion and remove defaults from overloads. --- sanic/mixins/listeners.py | 15 +++++++++------ tests/test_signal_handlers.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/sanic/mixins/listeners.py b/sanic/mixins/listeners.py index 3d0569a8c1..d38a01ed92 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 TYPE_CHECKING, Callable, List, Optional, Union, overload +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 @@ -31,7 +32,7 @@ def listener( self, listener_or_event: ListenerType[Sanic], event_or_none: str, - apply: bool = True, + apply: bool = ..., ) -> ListenerType[Sanic]: ... @@ -39,8 +40,8 @@ def listener( def listener( self, listener_or_event: str, - event_or_none: None = None, - apply: bool = True, + event_or_none: None = ..., + apply: bool = ..., ) -> Callable[[ListenerType[Sanic]], ListenerType[Sanic]]: ... @@ -82,8 +83,10 @@ def register_listener( return listener if callable(listener_or_event): - if TYPE_CHECKING: - assert event_or_none is not None + 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/tests/test_signal_handlers.py b/tests/test_signal_handlers.py index f7657ad64b..c6838b5e3a 100644 --- a/tests/test_signal_handlers.py +++ b/tests/test_signal_handlers.py @@ -10,6 +10,7 @@ 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 @@ async def atest(stop_first): 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)