RFC/1630 Signals (#2042)

* Temp working version of initial signal api

* fix signals router finalizing

* Additional tests

* Add event test

* finalize test

* remove old comment

* Add some missing annotations

* multiple apps per BP support

* deepsource?

* rtemove deepsource

* nominal change

* fix blueprints test

* trivial change to trigger build

* signal docstring

* squash

* squash

* Add a couple new tests

* Add some suggestions from review

* Remove inaccessible code

* Change where to condition
This commit is contained in:
Adam Hopkins
2021-03-14 10:09:07 +02:00
committed by GitHub
parent 1165663ec1
commit 824f41d6e0
16 changed files with 658 additions and 59 deletions

View File

@@ -7,7 +7,12 @@ import pytest
from sanic.app import Sanic
from sanic.blueprints import Blueprint
from sanic.constants import HTTP_METHODS
from sanic.exceptions import InvalidUsage, NotFound, ServerError
from sanic.exceptions import (
InvalidUsage,
NotFound,
SanicException,
ServerError,
)
from sanic.request import Request
from sanic.response import json, text
from sanic.views import CompositionView
@@ -18,6 +23,33 @@ from sanic.views import CompositionView
# ------------------------------------------------------------ #
def test_bp(app):
bp = Blueprint("test_text")
@bp.route("/")
def handler(request):
return text("Hello")
app.blueprint(bp)
request, response = app.test_client.get("/")
assert response.text == "Hello"
def test_bp_app_access(app):
bp = Blueprint("test")
with pytest.raises(
SanicException,
match="<Blueprint test> has not yet been registered to an app",
):
bp.apps
app.blueprint(bp)
assert app in bp.apps
@pytest.fixture(scope="module")
def static_file_directory():
"""The static directory to serve"""
@@ -62,19 +94,6 @@ def test_versioned_routes_get(app, method):
assert response.status == 200
def test_bp(app):
bp = Blueprint("test_text")
@bp.route("/")
def handler(request):
return text("Hello")
app.blueprint(bp)
request, response = app.test_client.get("/")
assert response.text == "Hello"
def test_bp_strict_slash(app):
bp = Blueprint("test_text")
@@ -988,3 +1007,20 @@ def test_blueprint_group_strict_slashes():
assert app.test_client.get("/v3/slash-check/bp2/r2")[1].status == 404
assert app.test_client.get("/v3/slash-check/bp2/r2/")[1].status == 200
assert app.test_client.get("/v2/other-prefix/bp3/r1")[1].status == 200
def test_blueprint_registered_multiple_apps():
app1 = Sanic("app1")
app2 = Sanic("app2")
bp = Blueprint("bp")
@bp.get("/")
async def handler(request):
return text(request.route.name)
app1.blueprint(bp)
app2.blueprint(bp)
for app in (app1, app2):
_, response = app.test_client.get("/")
assert response.text == f"{app.name}.bp.handler"

273
tests/test_signals.py Normal file
View File

@@ -0,0 +1,273 @@
import asyncio
from inspect import isawaitable
import pytest
from sanic_routing.exceptions import NotFound
from sanic import Blueprint
from sanic.exceptions import InvalidSignal, SanicException
def test_add_signal(app):
@app.signal("foo.bar.baz")
def sync_signal(*_):
...
@app.signal("foo.bar.baz")
async def async_signal(*_):
...
assert len(app.signal_router.routes) == 1
@pytest.mark.parametrize(
"signal",
(
"<foo>.bar.bax",
"foo.<bar>.baz",
"foo",
"foo.bar",
"foo.bar.baz.qux",
),
)
def test_invalid_signal(app, signal):
with pytest.raises(InvalidSignal, match=f"Invalid signal event: {signal}"):
@app.signal(signal)
def handler():
...
@pytest.mark.asyncio
async def test_dispatch_signal_triggers_multiple_handlers(app):
counter = 0
@app.signal("foo.bar.baz")
def sync_signal(*_):
nonlocal counter
counter += 1
@app.signal("foo.bar.baz")
async def async_signal(*_):
nonlocal counter
counter += 1
app.signal_router.finalize()
await app.dispatch("foo.bar.baz")
assert counter == 2
@pytest.mark.asyncio
async def test_dispatch_signal_triggers_triggers_event(app):
counter = 0
@app.signal("foo.bar.baz")
def sync_signal(*args):
nonlocal app
nonlocal counter
signal, *_ = app.signal_router.get("foo.bar.baz")
counter += signal.ctx.event.is_set()
app.signal_router.finalize()
await app.dispatch("foo.bar.baz")
signal, *_ = app.signal_router.get("foo.bar.baz")
assert counter == 1
@pytest.mark.asyncio
async def test_dispatch_signal_triggers_dynamic_route(app):
counter = 0
@app.signal("foo.bar.<baz:int>")
def sync_signal(baz):
nonlocal counter
counter += baz
app.signal_router.finalize()
await app.dispatch("foo.bar.9")
assert counter == 9
@pytest.mark.asyncio
async def test_dispatch_signal_triggers_with_requirements(app):
counter = 0
@app.signal("foo.bar.baz", condition={"one": "two"})
def sync_signal(*_):
nonlocal counter
counter += 1
app.signal_router.finalize()
await app.dispatch("foo.bar.baz")
assert counter == 0
await app.dispatch("foo.bar.baz", condition={"one": "two"})
assert counter == 1
@pytest.mark.asyncio
async def test_dispatch_signal_triggers_with_context(app):
counter = 0
@app.signal("foo.bar.baz")
def sync_signal(amount):
nonlocal counter
counter += amount
app.signal_router.finalize()
await app.dispatch("foo.bar.baz", context={"amount": 9})
assert counter == 9
@pytest.mark.asyncio
async def test_dispatch_signal_triggers_with_context_fail(app):
counter = 0
@app.signal("foo.bar.baz")
def sync_signal(amount):
nonlocal counter
counter += amount
app.signal_router.finalize()
with pytest.raises(TypeError):
await app.dispatch("foo.bar.baz", {"amount": 9})
@pytest.mark.asyncio
async def test_dispatch_signal_triggers_on_bp(app):
bp = Blueprint("bp")
app_counter = 0
bp_counter = 0
@app.signal("foo.bar.baz")
def app_signal():
nonlocal app_counter
app_counter += 1
@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")
assert app_counter == 1
assert bp_counter == 1
await bp.dispatch("foo.bar.baz")
assert app_counter == 1
assert bp_counter == 2
@pytest.mark.asyncio
async def test_dispatch_signal_triggers_event(app):
app_counter = 0
@app.signal("foo.bar.baz")
def app_signal():
...
async def do_wait():
nonlocal app_counter
await app.event("foo.bar.baz")
app_counter += 1
app.signal_router.finalize()
await app.dispatch("foo.bar.baz")
waiter = app.event("foo.bar.baz")
assert isawaitable(waiter)
fut = asyncio.ensure_future(do_wait())
await app.dispatch("foo.bar.baz")
await fut
assert app_counter == 1
@pytest.mark.asyncio
async def test_dispatch_signal_triggers_event_on_bp(app):
bp = Blueprint("bp")
bp_counter = 0
@bp.signal("foo.bar.baz")
def bp_signal():
...
async def do_wait():
nonlocal bp_counter
await bp.event("foo.bar.baz")
bp_counter += 1
app.blueprint(bp)
app.signal_router.finalize()
signal, *_ = app.signal_router.get(
"foo.bar.baz", condition={"blueprint": "bp"}
)
await bp.dispatch("foo.bar.baz")
waiter = bp.event("foo.bar.baz")
assert isawaitable(waiter)
fut = asyncio.ensure_future(do_wait())
signal.ctx.event.set()
await fut
assert bp_counter == 1
def test_bad_finalize(app):
counter = 0
@app.signal("foo.bar.baz")
def sync_signal(amount):
nonlocal counter
counter += amount
with pytest.raises(
RuntimeError, match="Cannot finalize signals outside of event loop"
):
app.signal_router.finalize()
assert counter == 0
def test_event_not_exist(app):
with pytest.raises(NotFound, match="Could not find signal does.not.exist"):
app.event("does.not.exist")
def test_event_not_exist_on_bp(app):
bp = Blueprint("bp")
app.blueprint(bp)
with pytest.raises(NotFound, match="Could not find signal does.not.exist"):
bp.event("does.not.exist")
def test_event_on_bp_not_registered():
bp = Blueprint("bp")
@bp.signal("foo.bar.baz")
def bp_signal():
...
with pytest.raises(
SanicException,
match="<Blueprint bp> has not yet been registered to an app",
):
bp.event("foo.bar.baz")