Repository: sanic-org/sanic
Pull request #2309 (2021-11-14 19:17:43)

Move builtin signals to enum
This is a convenience to help IDEs and consistency when using builtin signals. 

```python
from sanic.signals import Event
@app.signal(Event.HTTP_LIFECYCLE_BEGIN)
async def connect_open(**_):
    logger.debug("connect opened")
```

codeclimate[bot]: Code Climate has analyzed commit a8f13120 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **100.0%** (**86%** is the threshold).

This pull request will bring the total coverage in the repository to **86.6%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/sanic-org/sanic/pull/2309).

codeclimate[bot]: Code Climate has analyzed commit 60d02819 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **100.0%** (**86%** is the threshold).

This pull request will bring the total coverage in the repository to **86.6%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/sanic-org/sanic/pull/2309).


Patch:
diff --git a/sanic/blueprints.py b/sanic/blueprints.py
index e5e1d333..e13cafcd 100644
--- a/sanic/blueprints.py
+++ b/sanic/blueprints.py
@@ -4,6 +4,7 @@ import asyncio
 
 from collections import defaultdict
 from copy import deepcopy
+from enum import Enum
 from types import SimpleNamespace
 from typing import (
     TYPE_CHECKING,
@@ -144,7 +145,7 @@ class Blueprint(BaseSanic):
         kwargs["apply"] = False
         return super().exception(*args, **kwargs)
 
-    def signal(self, event: str, *args, **kwargs):
+    def signal(self, event: Union[str, Enum], *args, **kwargs):
         kwargs["apply"] = False
         return super().signal(event, *args, **kwargs)
 
diff --git a/sanic/mixins/signals.py b/sanic/mixins/signals.py
index 2be9fee2..57b01b46 100644
--- a/sanic/mixins/signals.py
+++ b/sanic/mixins/signals.py
@@ -1,4 +1,5 @@
-from typing import Any, Callable, Dict, Optional, Set
+from enum import Enum
+from typing import Any, Callable, Dict, Optional, Set, Union
 
 from sanic.models.futures import FutureSignal
 from sanic.models.handler_types import SignalHandler
@@ -19,7 +20,7 @@ class SignalMixin:
 
     def signal(
         self,
-        event: str,
+        event: Union[str, Enum],
         *,
         apply: bool = True,
         condition: Dict[str, Any] = None,
@@ -41,13 +42,11 @@ class SignalMixin:
             filtering, defaults to None
         :type condition: Dict[str, Any], optional
         """
+        event_value = str(event.value) if isinstance(event, Enum) else event
 
         def decorator(handler: SignalHandler):
-            nonlocal event
-            nonlocal apply
-
             future_signal = FutureSignal(
-                handler, event, HashableDict(condition or {})
+                handler, event_value, HashableDict(condition or {})
             )
             self._future_signals.add(future_signal)
 
diff --git a/sanic/signals.py b/sanic/signals.py
index 9da7eccd..7bb510fa 100644
--- a/sanic/signals.py
+++ b/sanic/signals.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import asyncio
 
+from enum import Enum
 from inspect import isawaitable
 from typing import Any, Dict, List, Optional, Tuple, Union
 
@@ -14,29 +15,47 @@ from sanic.log import error_logger, logger
 from sanic.models.handler_types import SignalHandler
 
 
+class Event(Enum):
+    SERVER_INIT_AFTER = "server.init.after"
+    SERVER_INIT_BEFORE = "server.init.before"
+    SERVER_SHUTDOWN_AFTER = "server.shutdown.after"
+    SERVER_SHUTDOWN_BEFORE = "server.shutdown.before"
+    HTTP_LIFECYCLE_BEGIN = "http.lifecycle.begin"
+    HTTP_LIFECYCLE_COMPLETE = "http.lifecycle.complete"
+    HTTP_LIFECYCLE_EXCEPTION = "http.lifecycle.exception"
+    HTTP_LIFECYCLE_HANDLE = "http.lifecycle.handle"
+    HTTP_LIFECYCLE_READ_BODY = "http.lifecycle.read_body"
+    HTTP_LIFECYCLE_READ_HEAD = "http.lifecycle.read_head"
+    HTTP_LIFECYCLE_REQUEST = "http.lifecycle.request"
+    HTTP_LIFECYCLE_RESPONSE = "http.lifecycle.response"
+    HTTP_ROUTING_AFTER = "http.routing.after"
+    HTTP_ROUTING_BEFORE = "http.routing.before"
+    HTTP_LIFECYCLE_SEND = "http.lifecycle.send"
+    HTTP_MIDDLEWARE_AFTER = "http.middleware.after"
+    HTTP_MIDDLEWARE_BEFORE = "http.middleware.before"
+
+
 RESERVED_NAMESPACES = {
     "server": (
-        # "server.main.start",
-        # "server.main.stop",
-        "server.init.before",
-        "server.init.after",
-        "server.shutdown.before",
-        "server.shutdown.after",
+        Event.SERVER_INIT_AFTER.value,
+        Event.SERVER_INIT_BEFORE.value,
+        Event.SERVER_SHUTDOWN_AFTER.value,
+        Event.SERVER_SHUTDOWN_BEFORE.value,
     ),
     "http": (
-        "http.lifecycle.begin",
-        "http.lifecycle.complete",
-        "http.lifecycle.exception",
-        "http.lifecycle.handle",
-        "http.lifecycle.read_body",
-        "http.lifecycle.read_head",
-        "http.lifecycle.request",
-        "http.lifecycle.response",
-        "http.routing.after",
-        "http.routing.before",
-        "http.lifecycle.send",
-        "http.middleware.after",
-        "http.middleware.before",
+        Event.HTTP_LIFECYCLE_BEGIN.value,
+        Event.HTTP_LIFECYCLE_COMPLETE.value,
+        Event.HTTP_LIFECYCLE_EXCEPTION.value,
+        Event.HTTP_LIFECYCLE_HANDLE.value,
+        Event.HTTP_LIFECYCLE_READ_BODY.value,
+        Event.HTTP_LIFECYCLE_READ_HEAD.value,
+        Event.HTTP_LIFECYCLE_REQUEST.value,
+        Event.HTTP_LIFECYCLE_RESPONSE.value,
+        Event.HTTP_ROUTING_AFTER.value,
+        Event.HTTP_ROUTING_BEFORE.value,
+        Event.HTTP_LIFECYCLE_SEND.value,
+        Event.HTTP_MIDDLEWARE_AFTER.value,
+        Event.HTTP_MIDDLEWARE_BEFORE.value,
     ),
 }
 


Test patch:
diff --git a/tests/test_signals.py b/tests/test_signals.py
index 9b8a9495..51aea3c8 100644
--- a/tests/test_signals.py
+++ b/tests/test_signals.py
@@ -1,5 +1,6 @@
 import asyncio
 
+from enum import Enum
 from inspect import isawaitable
 
 import pytest
@@ -50,6 +51,25 @@ def test_invalid_signal(app, signal):
             ...
 
 
+@pytest.mark.asyncio
+async def test_dispatch_signal_with_enum_event(app):
+    counter = 0
+
+    class FooEnum(Enum):
+        FOO_BAR_BAZ = "foo.bar.baz"
+
+    @app.signal(FooEnum.FOO_BAR_BAZ)
+    def sync_signal(*_):
+        nonlocal counter
+
+        counter += 1
+
+    app.signal_router.finalize()
+
+    await app.dispatch("foo.bar.baz")
+    assert counter == 1
+
+
 @pytest.mark.asyncio
 async def test_dispatch_signal_triggers_multiple_handlers(app):
     counter = 0