Change signal routing for increased consistency (#2277)

This commit is contained in:
Adam Hopkins 2021-12-24 01:27:54 +02:00 committed by GitHub
parent 8c07e388cd
commit b91ffed010
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 88 additions and 14 deletions

View File

@ -400,8 +400,9 @@ class Blueprint(BaseSanic):
for future in self._future_signals: for future in self._future_signals:
if (self, future) in app._future_registry: if (self, future) in app._future_registry:
continue continue
future.condition.update({"blueprint": self.name}) future.condition.update({"__blueprint__": self.name})
app._apply_signal(future) # Force exclusive to be False
app._apply_signal(tuple((*future[:-1], False)))
self.routes += [route for route in routes if isinstance(route, Route)] self.routes += [route for route in routes if isinstance(route, Route)]
self.websocket_routes += [ self.websocket_routes += [
@ -426,7 +427,7 @@ class Blueprint(BaseSanic):
async def dispatch(self, *args, **kwargs): async def dispatch(self, *args, **kwargs):
condition = kwargs.pop("condition", {}) condition = kwargs.pop("condition", {})
condition.update({"blueprint": self.name}) condition.update({"__blueprint__": self.name})
kwargs["condition"] = condition kwargs["condition"] = condition
await asyncio.gather( await asyncio.gather(
*[app.dispatch(*args, **kwargs) for app in self.apps] *[app.dispatch(*args, **kwargs) for app in self.apps]

View File

@ -21,6 +21,7 @@ class SignalMixin(metaclass=SanicMeta):
*, *,
apply: bool = True, apply: bool = True,
condition: Dict[str, Any] = None, condition: Dict[str, Any] = None,
exclusive: bool = True,
) -> Callable[[SignalHandler], SignalHandler]: ) -> Callable[[SignalHandler], SignalHandler]:
""" """
For creating a signal handler, used similar to a route handler: For creating a signal handler, used similar to a route handler:
@ -33,17 +34,22 @@ class SignalMixin(metaclass=SanicMeta):
:param event: Representation of the event in ``one.two.three`` form :param event: Representation of the event in ``one.two.three`` form
:type event: str :type event: str
:param apply: For lazy evaluation, defaults to True :param apply: For lazy evaluation, defaults to ``True``
:type apply: bool, optional :type apply: bool, optional
:param condition: For use with the ``condition`` argument in dispatch :param condition: For use with the ``condition`` argument in dispatch
filtering, defaults to None filtering, defaults to ``None``
:param exclusive: When ``True``, the signal can only be dispatched
when the condition has been met. When ``False``, the signal can
be dispatched either with or without it. *THIS IS INAPPLICABLE TO
BLUEPRINT SIGNALS. THEY ARE ALWAYS NON-EXCLUSIVE*, defaults
to ``True``
:type condition: Dict[str, Any], optional :type condition: Dict[str, Any], optional
""" """
event_value = str(event.value) if isinstance(event, Enum) else event event_value = str(event.value) if isinstance(event, Enum) else event
def decorator(handler: SignalHandler): def decorator(handler: SignalHandler):
future_signal = FutureSignal( future_signal = FutureSignal(
handler, event_value, HashableDict(condition or {}) handler, event_value, HashableDict(condition or {}), exclusive
) )
self._future_signals.add(future_signal) self._future_signals.add(future_signal)
@ -59,6 +65,7 @@ class SignalMixin(metaclass=SanicMeta):
handler: Optional[Callable[..., Any]], handler: Optional[Callable[..., Any]],
event: str, event: str,
condition: Dict[str, Any] = None, condition: Dict[str, Any] = None,
exclusive: bool = True,
): ):
if not handler: if not handler:
@ -66,7 +73,9 @@ class SignalMixin(metaclass=SanicMeta):
... ...
handler = noop handler = noop
self.signal(event=event, condition=condition)(handler) self.signal(event=event, condition=condition, exclusive=exclusive)(
handler
)
return handler return handler
def event(self, event: str): def event(self, event: str):

View File

@ -62,6 +62,7 @@ class FutureSignal(NamedTuple):
handler: SignalHandler handler: SignalHandler
event: str event: str
condition: Optional[Dict[str, str]] condition: Optional[Dict[str, str]]
exclusive: bool
class FutureRegistry(set): class FutureRegistry(set):

View File

@ -4,7 +4,7 @@ import asyncio
from enum import Enum from enum import Enum
from inspect import isawaitable from inspect import isawaitable
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union, cast
from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore
from sanic_routing.exceptions import NotFound # type: ignore from sanic_routing.exceptions import NotFound # type: ignore
@ -142,12 +142,21 @@ class SignalRouter(BaseRouter):
if context: if context:
params.update(context) params.update(context)
signals = group.routes
if not reverse: if not reverse:
handlers = handlers[::-1] signals = signals[::-1]
try: try:
for handler in handlers: for signal in signals:
if condition is None or condition == handler.__requirements__: params.pop("__trigger__", None)
maybe_coroutine = handler(**params) if (
(condition is None and signal.ctx.exclusive is False)
or (
condition is None
and not signal.handler.__requirements__
)
or (condition == signal.handler.__requirements__)
) and (signal.ctx.trigger or event == signal.ctx.definition):
maybe_coroutine = signal.handler(**params)
if isawaitable(maybe_coroutine): if isawaitable(maybe_coroutine):
retval = await maybe_coroutine retval = await maybe_coroutine
if retval: if retval:
@ -190,23 +199,36 @@ class SignalRouter(BaseRouter):
handler: SignalHandler, handler: SignalHandler,
event: str, event: str,
condition: Optional[Dict[str, Any]] = None, condition: Optional[Dict[str, Any]] = None,
exclusive: bool = True,
) -> Signal: ) -> Signal:
event_definition = event
parts = self._build_event_parts(event) parts = self._build_event_parts(event)
if parts[2].startswith("<"): if parts[2].startswith("<"):
name = ".".join([*parts[:-1], "*"]) name = ".".join([*parts[:-1], "*"])
trigger = self._clean_trigger(parts[2])
else: else:
name = event name = event
trigger = ""
if not trigger:
event = ".".join([*parts[:2], "<__trigger__>"])
handler.__requirements__ = condition # type: ignore handler.__requirements__ = condition # type: ignore
handler.__trigger__ = trigger # type: ignore
return super().add( signal = super().add(
event, event,
handler, handler,
requirements=condition,
name=name, name=name,
append=True, append=True,
) # type: ignore ) # type: ignore
signal.ctx.exclusive = exclusive
signal.ctx.trigger = trigger
signal.ctx.definition = event_definition
return cast(Signal, signal)
def finalize(self, do_compile: bool = True, do_optimize: bool = False): def finalize(self, do_compile: bool = True, do_optimize: bool = False):
self.add(_blank, "sanic.__signal__.__init__") self.add(_blank, "sanic.__signal__.__init__")
@ -238,3 +260,9 @@ class SignalRouter(BaseRouter):
"Cannot declare reserved signal event: %s" % event "Cannot declare reserved signal event: %s" % event
) )
return parts return parts
def _clean_trigger(self, trigger: str) -> str:
trigger = trigger[1:-1]
if ":" in trigger:
trigger, _ = trigger.split(":")
return trigger

View File

@ -145,6 +145,23 @@ async def test_dispatch_signal_triggers_with_requirements(app):
assert counter == 1 assert counter == 1
@pytest.mark.asyncio
async def test_dispatch_signal_triggers_with_requirements_exclusive(app):
counter = 0
@app.signal("foo.bar.baz", condition={"one": "two"}, exclusive=False)
def sync_signal(*_):
nonlocal counter
counter += 1
app.signal_router.finalize()
await app.dispatch("foo.bar.baz")
assert counter == 1
await app.dispatch("foo.bar.baz", condition={"one": "two"})
assert counter == 2
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_dispatch_signal_triggers_with_context(app): async def test_dispatch_signal_triggers_with_context(app):
counter = 0 counter = 0
@ -204,6 +221,24 @@ async def test_dispatch_signal_triggers_on_bp(app):
assert bp_counter == 2 assert bp_counter == 2
@pytest.mark.asyncio
async def test_dispatch_signal_triggers_on_bp_alone(app):
bp = Blueprint("bp")
bp_counter = 0
@bp.signal("foo.bar.baz")
def bp_signal():
nonlocal bp_counter
bp_counter += 1
app.blueprint(bp)
app.signal_router.finalize()
await app.dispatch("foo.bar.baz")
await bp.dispatch("foo.bar.baz")
assert bp_counter == 2
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_dispatch_signal_triggers_event(app): async def test_dispatch_signal_triggers_event(app):
app_counter = 0 app_counter = 0