Skip to content

Commit

Permalink
Change signal routing for increased consistency (sanic-org#2277)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahopkins authored and ChihweiLHBird committed Jun 1, 2022
1 parent b4f2da4 commit 259465d
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 14 deletions.
7 changes: 4 additions & 3 deletions sanic/blueprints.py
Expand Up @@ -400,8 +400,9 @@ def register(self, app, options):
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 += [
Expand All @@ -426,7 +427,7 @@ def register(self, app, options):

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]
Expand Down
17 changes: 13 additions & 4 deletions sanic/mixins/signals.py
Expand Up @@ -21,6 +21,7 @@ def signal(
*,
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:
Expand All @@ -33,17 +34,22 @@ async def signal_handler(thing, **kwargs):
: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)

Expand All @@ -59,14 +65,17 @@ def add_signal(
handler: Optional[Callable[..., Any]],
event: str,
condition: Dict[str, Any] = None,
exclusive: bool = True,
):
if not handler:

async def noop():
...

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):
Expand Down
1 change: 1 addition & 0 deletions sanic/models/futures.py
Expand Up @@ -62,6 +62,7 @@ class FutureSignal(NamedTuple):
handler: SignalHandler
event: str
condition: Optional[Dict[str, str]]
exclusive: bool


class FutureRegistry(set):
Expand Down
42 changes: 35 additions & 7 deletions sanic/signals.py
Expand Up @@ -4,7 +4,7 @@

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
Expand Down Expand Up @@ -142,12 +142,21 @@ async def _dispatch(
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:
Expand Down Expand Up @@ -190,23 +199,36 @@ def add( # type: ignore
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__")

Expand Down Expand Up @@ -238,3 +260,9 @@ def _build_event_parts(self, event: str) -> Tuple[str, str, str]:
"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
35 changes: 35 additions & 0 deletions tests/test_signals.py
Expand Up @@ -145,6 +145,23 @@ def sync_signal(*_):
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
Expand Down Expand Up @@ -204,6 +221,24 @@ def bp_signal():
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
Expand Down

0 comments on commit 259465d

Please sign in to comment.