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

RFC/1630 Signals #2042

Merged
merged 31 commits into from
Mar 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a268087
Temp working version of initial signal api
ahopkins Feb 23, 2021
25b170a
Merge branch 'master' into signals-api
ahopkins Feb 28, 2021
5548acb
Add signals testing
ahopkins Mar 4, 2021
c506f54
fix signals router finalizing
ahopkins Mar 4, 2021
93371c3
merge conflict
ahopkins Mar 4, 2021
93f87a1
Use new sanic-router to resolve app/bp on same signal path
ahopkins Mar 8, 2021
1646a40
Additional tests
ahopkins Mar 8, 2021
fbc5d25
Add event test
ahopkins Mar 8, 2021
e54d7c6
finalize test
ahopkins Mar 8, 2021
ded054f
remove old comment
ahopkins Mar 8, 2021
63a50ba
Add some missing annotations
ahopkins Mar 8, 2021
6feb8e9
multiple apps per BP support
ahopkins Mar 9, 2021
5b3cfba
Merge branch 'master' of github.com:sanic-org/sanic into signals-api
ahopkins Mar 9, 2021
d2fa5e4
deepsource?
ahopkins Mar 9, 2021
6dbc830
rtemove deepsource
ahopkins Mar 9, 2021
b014b29
nominal change
ahopkins Mar 9, 2021
4a1343c
fix blueprints test
ahopkins Mar 9, 2021
c83e7b1
Merge branch 'master' into signals-api
ahopkins Mar 10, 2021
555fb1d
Merge branch 'master' into signals-api
ahopkins Mar 11, 2021
470c9b5
trivial change to trigger build
ahopkins Mar 11, 2021
14cdc7a
Merge branch 'signals-api' of github.com:sanic-org/sanic into signals…
ahopkins Mar 11, 2021
b04733e
signal docstring
ahopkins Mar 11, 2021
53d2d70
Updates from feedback
ahopkins Mar 11, 2021
ccbe4eb
squash
ahopkins Mar 11, 2021
4555b55
squash
ahopkins Mar 11, 2021
4a7252e
Add a couple new tests
ahopkins Mar 11, 2021
821e380
Merge branch 'master' into signals-api
ahopkins Mar 12, 2021
472d121
Add some suggestions from review
ahopkins Mar 13, 2021
4917948
Merge branch 'signals-api' of github.com:sanic-org/sanic into signals…
ahopkins Mar 13, 2021
f0687bb
Remove inaccessible code
ahopkins Mar 14, 2021
5c1e146
Change where to condition
ahopkins Mar 14, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions .deepsource.toml

This file was deleted.

86 changes: 64 additions & 22 deletions sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
import os
import re

from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
from asyncio import (
CancelledError,
Protocol,
ensure_future,
get_event_loop,
wait_for,
)
from asyncio.futures import Future
from collections import defaultdict, deque
from functools import partial
Expand All @@ -13,7 +19,9 @@
from traceback import format_exc
from typing import (
Any,
Awaitable,
Callable,
Coroutine,
Deque,
Dict,
Iterable,
Expand All @@ -26,6 +34,7 @@
from urllib.parse import urlencode, urlunparse

from sanic_routing.exceptions import FinalizationError # type: ignore
from sanic_routing.exceptions import NotFound # type: ignore
from sanic_routing.route import Route # type: ignore

from sanic import reloader_helpers
Expand All @@ -48,20 +57,17 @@
FutureListener,
FutureMiddleware,
FutureRoute,
FutureSignal,
FutureStatic,
)
from sanic.models.handler_types import ListenerType, MiddlewareType
from sanic.request import Request
from sanic.response import BaseHTTPResponse, HTTPResponse
from sanic.router import Router
from sanic.server import (
AsyncioServer,
HttpProtocol,
Signal,
serve,
serve_multiple,
serve_single,
)
from sanic.server import AsyncioServer, HttpProtocol
from sanic.server import Signal as ServerSignal
from sanic.server import serve, serve_multiple, serve_single
from sanic.signals import Signal, SignalRouter
from sanic.websocket import ConnectionClosed, WebSocketProtocol


Expand All @@ -76,10 +82,11 @@ class Sanic(BaseSanic):
def __init__(
self,
name: str = None,
router: Router = None,
error_handler: ErrorHandler = None,
router: Optional[Router] = None,
signal_router: Optional[SignalRouter] = None,
error_handler: Optional[ErrorHandler] = None,
load_env: bool = True,
request_class: Type[Request] = None,
request_class: Optional[Type[Request]] = None,
strict_slashes: bool = False,
log_config: Optional[Dict[str, Any]] = None,
configure_logging: bool = True,
Expand All @@ -100,6 +107,7 @@ def __init__(
self.name = name
self.asgi = False
self.router = router or Router()
self.signal_router = signal_router or SignalRouter()
self.request_class = request_class
self.error_handler = error_handler or ErrorHandler()
self.config = Config(load_env=load_env)
Expand Down Expand Up @@ -162,7 +170,7 @@ def add_task(self, task) -> None:
also return a future, and the actual ensure_future call
is delayed until before server start.

`See user guide
`See user guide re: background tasks
<https://sanicframework.org/guide/basics/tasks.html#background-tasks>`__

:param task: future, couroutine or awaitable
Expand Down Expand Up @@ -309,6 +317,28 @@ def _apply_middleware(
middleware.middleware, middleware.attach_to
)

def _apply_signal(self, signal: FutureSignal) -> Signal:
return self.signal_router.add(*signal)

def dispatch(
self,
event: str,
*,
condition: Optional[Dict[str, str]] = None,
context: Optional[Dict[str, Any]] = None,
) -> Coroutine[Any, Any, Awaitable[Any]]:
return self.signal_router.dispatch(
event,
context=context,
condition=condition,
)

def event(self, event: str, timeout: Optional[Union[int, float]] = None):
signal = self.signal_router.name_index.get(event)
if not signal:
raise NotFound("Could not find signal %s" % event)
return wait_for(signal.ctx.event.wait(), timeout=timeout)

def enable_websocket(self, enable=True):
"""Enable or disable the support for websocket.

Expand Down Expand Up @@ -382,7 +412,7 @@ def url_for(self, view_name: str, **kwargs):

app.config.SERVER_NAME = "myserver:7777"

`See user guide
`See user guide re: routing
<https://sanicframework.org/guide/basics/routing.html#generating-a-url>`__

:param view_name: string referencing the view name
Expand Down Expand Up @@ -1031,11 +1061,9 @@ def _helper(
):
"""Helper function used by `run` and `create_server`."""

try:
self.router.finalize()
except FinalizationError as e:
if not Sanic.test_mode:
raise e
self.listeners["before_server_start"] = [
self.finalize
] + self.listeners["before_server_start"]

if isinstance(ssl, dict):
# try common aliaseses
Expand Down Expand Up @@ -1064,7 +1092,7 @@ def _helper(
"unix": unix,
"ssl": ssl,
"app": self,
"signal": Signal(),
"signal": ServerSignal(),
"loop": loop,
"register_sys_signals": register_sys_signals,
"backlog": backlog,
Expand Down Expand Up @@ -1159,7 +1187,7 @@ def update_config(self, config: Union[bytes, str, dict, Any]):
"""
Update app.config. Full implementation can be found in the user guide.

`See user guide
`See user guide re: configuration
<https://sanicframework.org/guide/deployment/configuration.html#basics>`__
"""

Expand Down Expand Up @@ -1196,7 +1224,7 @@ def get_app(
'Multiple Sanic apps found, use Sanic.get_app("app_name")'
)
elif len(cls._app_registry) == 0:
raise SanicException(f"No Sanic apps have been registered.")
raise SanicException("No Sanic apps have been registered.")
else:
return list(cls._app_registry.values())[0]
try:
Expand All @@ -1205,3 +1233,17 @@ def get_app(
if force_create:
return cls(name)
raise SanicException(f'Sanic app name "{name}" not found.')

# -------------------------------------------------------------------- #
# Static methods
# -------------------------------------------------------------------- #

@staticmethod
async def finalize(app, _):
try:
app.router.finalize()
if app.signal_router.routes:
app.signal_router.finalize() # noqa
except FinalizationError as e:
if not Sanic.test_mode:
raise e # noqa
2 changes: 2 additions & 0 deletions sanic/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ async def startup(self) -> None:
startup event.
"""
self.asgi_app.sanic_app.router.finalize()
if self.asgi_app.sanic_app.signal_router.routes:
self.asgi_app.sanic_app.signal_router.finalize()
listeners = self.asgi_app.sanic_app.listeners.get(
"before_server_start", []
) + self.asgi_app.sanic_app.listeners.get("after_server_start", [])
Expand Down
2 changes: 2 additions & 0 deletions sanic/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from sanic.mixins.listeners import ListenerMixin
from sanic.mixins.middleware import MiddlewareMixin
from sanic.mixins.routes import RouteMixin
from sanic.mixins.signals import SignalMixin


class Base(type):
Expand Down Expand Up @@ -31,6 +32,7 @@ class BaseSanic(
MiddlewareMixin,
ListenerMixin,
ExceptionMixin,
SignalMixin,
metaclass=Base,
):
def __str__(self) -> str:
Expand Down
54 changes: 52 additions & 2 deletions sanic/blueprints.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
from __future__ import annotations

import asyncio

from collections import defaultdict
from typing import Dict, Iterable, List, Optional
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union

from sanic_routing.exceptions import NotFound # type: ignore
from sanic_routing.route import Route # type: ignore

from sanic.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup
from sanic.exceptions import SanicException
from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import (
ListenerType,
Expand All @@ -13,6 +19,10 @@
)


if TYPE_CHECKING:
from sanic import Sanic # noqa


class Blueprint(BaseSanic):
"""
In *Sanic* terminology, a **Blueprint** is a logical collection of
Expand All @@ -21,7 +31,7 @@ class Blueprint(BaseSanic):

It is the main tool for grouping functionality and similar endpoints.

`See user guide
`See user guide re: blueprints
<https://sanicframework.org/guide/best-practices/blueprints.html>`__

:param name: unique name of the blueprint
Expand All @@ -40,6 +50,7 @@ def __init__(
version: Optional[int] = None,
strict_slashes: Optional[bool] = None,
):
self._apps: Set[Sanic] = set()
self.name = name
self.url_prefix = url_prefix
self.host = host
Expand Down Expand Up @@ -70,6 +81,14 @@ def __repr__(self) -> str:
)
return f"Blueprint({args})"

@property
def apps(self):
if not self._apps:
raise SanicException(
f"{self} has not yet been registered to an app"
)
return self._apps

def route(self, *args, **kwargs):
kwargs["apply"] = False
return super().route(*args, **kwargs)
Expand All @@ -90,6 +109,10 @@ def exception(self, *args, **kwargs):
kwargs["apply"] = False
return super().exception(*args, **kwargs)

def signal(self, event: str, *args, **kwargs):
kwargs["apply"] = False
return super().signal(event, *args, **kwargs)

@staticmethod
def group(*blueprints, url_prefix="", version=None, strict_slashes=None):
"""
Expand Down Expand Up @@ -132,6 +155,7 @@ def register(self, app, options):
*url_prefix* - URL Prefix to override the blueprint prefix
"""

self._apps.add(app)
url_prefix = options.get("url_prefix", self.url_prefix)

routes = []
Expand Down Expand Up @@ -200,6 +224,10 @@ def register(self, app, options):
for listener in self._future_listeners:
listeners[listener.event].append(app._apply_listener(listener))

for signal in self._future_signals:
signal.condition.update({"blueprint": self.name})
app._apply_signal(signal)

self.routes = [route for route in routes if isinstance(route, Route)]

# Deprecate these in 21.6
Expand All @@ -209,3 +237,25 @@ def register(self, app, options):
self.middlewares = middleware
self.exceptions = exception_handlers
self.listeners = dict(listeners)

async def dispatch(self, *args, **kwargs):
condition = kwargs.pop("condition", {})
condition.update({"blueprint": self.name})
kwargs["condition"] = condition
await asyncio.gather(
*[app.dispatch(*args, **kwargs) for app in self.apps]
)

def event(self, event: str, timeout: Optional[Union[int, float]] = None):
events = set()
for app in self.apps:
signal = app.signal_router.name_index.get(event)
if not signal:
raise NotFound("Could not find signal %s" % event)
events.add(signal.ctx.event)
Comment on lines +250 to +255
Copy link
Member

@ashleysommer ashleysommer Mar 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this BP is registered on multiple apps, does every app need to have a signal in its router? In the current code this will throw an error if any of the apps doesn't have the signal. I think this an editing error, I think the correct logic is:

Suggested change
events = set()
for app in self.apps:
signal = app.signal_router.name_index.get(event)
if not signal:
raise NotFound("Could not find signal %s" % event)
events.add(signal.ctx.event)
events = set()
for app in self.apps:
signal = app.signal_router.name_index.get(event)
if signal:
events.add(signal.ctx.event)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you register a BP, all of its signals should be on the app. In the case where there are multiple apps, it should be on all of them. If it is missing from one, well, likely something went wrong somewhere.

I am fine making this change, but it still feels to me like something bad just happened that should be handled here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, if it was your intention to do it that way, thats fine. It just looked like a copy&paste error when I was looking through the code, like that exception wasn't supposed to be there. (Eg, if the exception is thrown in the for loop, there is no way for the NotFound exception at the end of the function to be hit).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back and forth on this myself a couple times. I think maybe you are right though that the final exception doesn't quite fit.

That exception would only be hit if there are no registered apps, in which caste the exception NotFound doesn't make sense.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And, because we are accessing self.apps as a property, you would get an exception earlier than that before it ever reaches there.

I suppose I was thinking that we needed to catch when events was empty, but I am not seeing how it could be.

Anyone see something else?


return asyncio.wait(
[event.wait() for event in events],
return_when=asyncio.FIRST_COMPLETED,
timeout=timeout,
)
2 changes: 1 addition & 1 deletion sanic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class C:

config.update_config(C)

`See user guide
`See user guide re: config
<https://sanicframework.org/guide/deployment/configuration.html>`__
"""

Expand Down
4 changes: 4 additions & 0 deletions sanic/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ class LoadFileException(SanicException):
pass


class InvalidSignal(SanicException):
pass


def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
"""
Raise an exception based on SanicException. Returns the HTTP response
Expand Down
4 changes: 2 additions & 2 deletions sanic/mixins/listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ def listener(
async def before_server_start(app, loop):
...

`See user guide
<https://sanicframework.org/guide/basics/listeners.html#listeners>`_
`See user guide re: listeners
<https://sanicframework.org/guide/basics/listeners.html#listeners>`__

:param event: event to listen to
"""
Expand Down
4 changes: 2 additions & 2 deletions sanic/mixins/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ def middleware(
Can either be called as *@app.middleware* or
*@app.middleware('request')*

`See user guide
<https://sanicframework.org/guide/basics/middleware.html>`_
`See user guide re: middleware
<https://sanicframework.org/guide/basics/middleware.html>`__

:param: middleware_or_request: Optional parameter to use for
identifying which type of middleware is being registered.
Expand Down
2 changes: 1 addition & 1 deletion sanic/mixins/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(self, *args, **kwargs) -> None:
self.name = ""
self.strict_slashes: Optional[bool] = False

def _apply_route(self, route: FutureRoute) -> Route:
def _apply_route(self, route: FutureRoute) -> List[Route]:
raise NotImplementedError # noqa

def _apply_static(self, static: FutureStatic) -> Route:
Expand Down
Loading