diff --git a/sanic/app.py b/sanic/app.py index 67ba68fbce..0f6241bea5 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1511,7 +1511,8 @@ def finalize(self): if not Sanic.test_mode: raise e - def signalize(self): + def signalize(self, allow_fail_builtin=True): + self.signal_router.allow_fail_builtin = allow_fail_builtin try: self.signal_router.finalize() except FinalizationError as e: @@ -1526,8 +1527,11 @@ async def _startup(self): if hasattr(self, "_ext"): self.ext._display() + if self.state.is_debug: + self.config.TOUCHUP = False + # Setup routers - self.signalize() + self.signalize(self.config.TOUCHUP) self.finalize() # TODO: Replace in v22.6 to check against apps in app registry @@ -1547,7 +1551,8 @@ async def _startup(self): # TODO: # - Raise warning if secondary apps have error handler config ErrorHandler.finalize(self.error_handler, config=self.config) - TouchUp.run(self) + if self.config.TOUCHUP: + TouchUp.run(self) self.state.is_started = True diff --git a/sanic/config.py b/sanic/config.py index 2e387ebd70..a07a0f4c03 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -38,6 +38,7 @@ "REQUEST_MAX_SIZE": 100000000, # 100 megabytes "REQUEST_TIMEOUT": 60, # 60 seconds "RESPONSE_TIMEOUT": 60, # 60 seconds + "TOUCHUP": True, "USE_UVLOOP": _default, "WEBSOCKET_MAX_SIZE": 2**20, # 1 megabyte "WEBSOCKET_PING_INTERVAL": 20, @@ -81,6 +82,7 @@ class Config(dict, metaclass=DescriptorMeta): REQUEST_TIMEOUT: int RESPONSE_TIMEOUT: int SERVER_NAME: str + TOUCHUP: bool USE_UVLOOP: Union[Default, bool] WEBSOCKET_MAX_SIZE: int WEBSOCKET_PING_INTERVAL: int diff --git a/sanic/signals.py b/sanic/signals.py index f4061b69cc..d62a117c52 100644 --- a/sanic/signals.py +++ b/sanic/signals.py @@ -80,6 +80,7 @@ def __init__(self) -> None: group_class=SignalGroup, stacking=True, ) + self.allow_fail_builtin = True self.ctx.loop = None def get( # type: ignore @@ -129,7 +130,8 @@ async def _dispatch( try: group, handlers, params = self.get(event, condition=condition) except NotFound as e: - if fail_not_found: + is_reserved = event.split(".", 1)[0] in RESERVED_NAMESPACES + if fail_not_found and (not is_reserved or self.allow_fail_builtin): raise e else: if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1: diff --git a/tests/test_touchup.py b/tests/test_touchup.py index 031a15e80d..8514268544 100644 --- a/tests/test_touchup.py +++ b/tests/test_touchup.py @@ -2,6 +2,8 @@ import pytest +from sanic_routing.exceptions import NotFound + from sanic.signals import RESERVED_NAMESPACES from sanic.touchup import TouchUp @@ -28,3 +30,50 @@ async def test_ode_removes_dispatch_events(app, caplog, verbosity, result): ) in logs ) is result + + +@pytest.mark.parametrize("skip_it,result", ((False, True), (True, False))) +async def test_skip_touchup(app, caplog, skip_it, result): + app.config.TOUCHUP = not skip_it + with caplog.at_level(logging.DEBUG, logger="sanic.root"): + app.state.verbosity = 2 + await app._startup() + assert app.signal_router.allow_fail_builtin is (not skip_it) + logs = caplog.record_tuples + + for signal in RESERVED_NAMESPACES["http"]: + assert ( + ( + "sanic.root", + logging.DEBUG, + f"Disabling event: {signal}", + ) + in logs + ) is result + not_found_exceptions = 0 + # Skip-touchup disables NotFound exceptions on the dispatcher + for signal in RESERVED_NAMESPACES["http"]: + try: + await app.dispatch(event=signal, inline=True) + except NotFound: + not_found_exceptions += 1 + assert (not_found_exceptions > 0) is result + + +@pytest.mark.parametrize("skip_it,result", ((False, True), (True, True))) +async def test_skip_touchup_non_reserved(app, caplog, skip_it, result): + app.config.TOUCHUP = not skip_it + + @app.signal("foo.bar.one") + def sync_signal(*_): + ... + + await app._startup() + assert app.signal_router.allow_fail_builtin is (not skip_it) + not_found_exception = False + # Skip-touchup doesn't disable NotFound exceptions for user-defined signals + try: + await app.dispatch(event="foo.baz.two", inline=True) + except NotFound: + not_found_exception = True + assert not_found_exception is result