Change signal routing for increased consistency (#2277)
This commit is contained in:
parent
8c07e388cd
commit
b91ffed010
|
@ -400,8 +400,9 @@ class Blueprint(BaseSanic):
|
|||
for future in self._future_signals:
|
||||
if (self, future) in app._future_registry:
|
||||
continue
|
||||
future.condition.update({"blueprint": self.name})
|
||||
app._apply_signal(future)
|
||||
future.condition.update({"__blueprint__": self.name})
|
||||
# Force exclusive to be False
|
||||
app._apply_signal(tuple((*future[:-1], False)))
|
||||
|
||||
self.routes += [route for route in routes if isinstance(route, Route)]
|
||||
self.websocket_routes += [
|
||||
|
@ -426,7 +427,7 @@ class Blueprint(BaseSanic):
|
|||
|
||||
async def dispatch(self, *args, **kwargs):
|
||||
condition = kwargs.pop("condition", {})
|
||||
condition.update({"blueprint": self.name})
|
||||
condition.update({"__blueprint__": self.name})
|
||||
kwargs["condition"] = condition
|
||||
await asyncio.gather(
|
||||
*[app.dispatch(*args, **kwargs) for app in self.apps]
|
||||
|
|
|
@ -21,6 +21,7 @@ class SignalMixin(metaclass=SanicMeta):
|
|||
*,
|
||||
apply: bool = True,
|
||||
condition: Dict[str, Any] = None,
|
||||
exclusive: bool = True,
|
||||
) -> Callable[[SignalHandler], SignalHandler]:
|
||||
"""
|
||||
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
|
||||
:type event: str
|
||||
:param apply: For lazy evaluation, defaults to True
|
||||
:param apply: For lazy evaluation, defaults to ``True``
|
||||
:type apply: bool, optional
|
||||
: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
|
||||
"""
|
||||
event_value = str(event.value) if isinstance(event, Enum) else event
|
||||
|
||||
def decorator(handler: SignalHandler):
|
||||
future_signal = FutureSignal(
|
||||
handler, event_value, HashableDict(condition or {})
|
||||
handler, event_value, HashableDict(condition or {}), exclusive
|
||||
)
|
||||
self._future_signals.add(future_signal)
|
||||
|
||||
|
@ -59,6 +65,7 @@ class SignalMixin(metaclass=SanicMeta):
|
|||
handler: Optional[Callable[..., Any]],
|
||||
event: str,
|
||||
condition: Dict[str, Any] = None,
|
||||
exclusive: bool = True,
|
||||
):
|
||||
if not handler:
|
||||
|
||||
|
@ -66,7 +73,9 @@ class SignalMixin(metaclass=SanicMeta):
|
|||
...
|
||||
|
||||
handler = noop
|
||||
self.signal(event=event, condition=condition)(handler)
|
||||
self.signal(event=event, condition=condition, exclusive=exclusive)(
|
||||
handler
|
||||
)
|
||||
return handler
|
||||
|
||||
def event(self, event: str):
|
||||
|
|
|
@ -62,6 +62,7 @@ class FutureSignal(NamedTuple):
|
|||
handler: SignalHandler
|
||||
event: str
|
||||
condition: Optional[Dict[str, str]]
|
||||
exclusive: bool
|
||||
|
||||
|
||||
class FutureRegistry(set):
|
||||
|
|
|
@ -4,7 +4,7 @@ import asyncio
|
|||
|
||||
from enum import Enum
|
||||
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.exceptions import NotFound # type: ignore
|
||||
|
@ -142,12 +142,21 @@ class SignalRouter(BaseRouter):
|
|||
if context:
|
||||
params.update(context)
|
||||
|
||||
signals = group.routes
|
||||
if not reverse:
|
||||
handlers = handlers[::-1]
|
||||
signals = signals[::-1]
|
||||
try:
|
||||
for handler in handlers:
|
||||
if condition is None or condition == handler.__requirements__:
|
||||
maybe_coroutine = handler(**params)
|
||||
for signal in signals:
|
||||
params.pop("__trigger__", None)
|
||||
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):
|
||||
retval = await maybe_coroutine
|
||||
if retval:
|
||||
|
@ -190,23 +199,36 @@ class SignalRouter(BaseRouter):
|
|||
handler: SignalHandler,
|
||||
event: str,
|
||||
condition: Optional[Dict[str, Any]] = None,
|
||||
exclusive: bool = True,
|
||||
) -> Signal:
|
||||
event_definition = event
|
||||
parts = self._build_event_parts(event)
|
||||
if parts[2].startswith("<"):
|
||||
name = ".".join([*parts[:-1], "*"])
|
||||
trigger = self._clean_trigger(parts[2])
|
||||
else:
|
||||
name = event
|
||||
trigger = ""
|
||||
|
||||
if not trigger:
|
||||
event = ".".join([*parts[:2], "<__trigger__>"])
|
||||
|
||||
handler.__requirements__ = condition # type: ignore
|
||||
handler.__trigger__ = trigger # type: ignore
|
||||
|
||||
return super().add(
|
||||
signal = super().add(
|
||||
event,
|
||||
handler,
|
||||
requirements=condition,
|
||||
name=name,
|
||||
append=True,
|
||||
) # 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):
|
||||
self.add(_blank, "sanic.__signal__.__init__")
|
||||
|
||||
|
@ -238,3 +260,9 @@ class SignalRouter(BaseRouter):
|
|||
"Cannot declare reserved signal event: %s" % event
|
||||
)
|
||||
return parts
|
||||
|
||||
def _clean_trigger(self, trigger: str) -> str:
|
||||
trigger = trigger[1:-1]
|
||||
if ":" in trigger:
|
||||
trigger, _ = trigger.split(":")
|
||||
return trigger
|
||||
|
|
|
@ -145,6 +145,23 @@ async def test_dispatch_signal_triggers_with_requirements(app):
|
|||
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
|
||||
async def test_dispatch_signal_triggers_with_context(app):
|
||||
counter = 0
|
||||
|
@ -204,6 +221,24 @@ async def test_dispatch_signal_triggers_on_bp(app):
|
|||
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
|
||||
async def test_dispatch_signal_triggers_event(app):
|
||||
app_counter = 0
|
||||
|
|
Loading…
Reference in New Issue
Block a user