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:
|
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]
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user