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:
@@ -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
273
tests/test_signals.py
Normal 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")
|
||||
Reference in New Issue
Block a user