Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change signal routing for increased consistency #2277

Merged
merged 7 commits into from Dec 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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