Repository: sanic-org/sanic
Pull request #2813 (2023-08-30 09:48:54)

Allow for simple signals
The point of this PR is to allow for definition and use of simple signals. Sometimes, you do not need the full three part scheme.

```python
@app.signal("<thing>")
async def handler(**kwargs):
    print("foobar signal received")
    print(kwargs)


@app.route("/")
async def test(request: Request):
    await request.app.dispatch("foobar")
    return json({"hello": "world"})
```

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sanic-org/sanic/pull/2813?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`7d93c10`)](https://app.codecov.io/gh/sanic-org/sanic/commit/7d93c106029d58bb43681ff93e39aa0aa9ec9f2b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) 88.348% compared to head [(`436055f`)](https://app.codecov.io/gh/sanic-org/sanic/pull/2813?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) 88.364%.


<details><summary>Additional details and impacted files</summary>


```diff
@@              Coverage Diff              @@
##              main     #2813       +/-   ##
=============================================
+ Coverage   88.348%   88.364%   +0.016%     
=============================================
  Files           92        92               
  Lines         7175      7185       +10     
  Branches      1232      1233        +1     
=============================================
+ Hits          6339      6349       +10     
  Misses         581       581               
  Partials       255       255               
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/sanic-org/sanic/pull/2813?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).


Patch:
diff --git a/sanic/http/tls/context.py b/sanic/http/tls/context.py
index b210530f..3402df29 100644
--- a/sanic/http/tls/context.py
+++ b/sanic/http/tls/context.py
@@ -39,7 +39,7 @@ def create_context(
 
 
 def shorthand_to_ctx(
-    ctxdef: Union[None, ssl.SSLContext, dict, str]
+    ctxdef: Union[None, ssl.SSLContext, dict, str],
 ) -> Optional[ssl.SSLContext]:
     """Convert an ssl argument shorthand to an SSLContext object."""
     if ctxdef is None or isinstance(ctxdef, ssl.SSLContext):
@@ -55,7 +55,7 @@ def shorthand_to_ctx(
 
 
 def process_to_context(
-    ssldef: Union[None, ssl.SSLContext, dict, str, list, tuple]
+    ssldef: Union[None, ssl.SSLContext, dict, str, list, tuple],
 ) -> Optional[ssl.SSLContext]:
     """Process app.run ssl argument from easy formats to full SSLContext."""
     return (
diff --git a/sanic/mixins/startup.py b/sanic/mixins/startup.py
index 5e640fed..371be6d2 100644
--- a/sanic/mixins/startup.py
+++ b/sanic/mixins/startup.py
@@ -846,7 +846,7 @@ class StartupMixin(metaclass=SanicMeta):
 
     @staticmethod
     def get_server_location(
-        server_settings: Optional[Dict[str, Any]] = None
+        server_settings: Optional[Dict[str, Any]] = None,
     ) -> str:
         """Using the server settings, retrieve the server location.
 
diff --git a/sanic/server/socket.py b/sanic/server/socket.py
index a77e80a0..7a0074d4 100644
--- a/sanic/server/socket.py
+++ b/sanic/server/socket.py
@@ -94,7 +94,7 @@ def remove_unix_socket(path: Optional[str]) -> None:
 
 
 def configure_socket(
-    server_settings: Dict[str, Any]
+    server_settings: Dict[str, Any],
 ) -> Optional[socket.SocketType]:
     # Create a listening socket or use the one in settings
     if server_settings.get("version") is HTTP.VERSION_3:
diff --git a/sanic/signals.py b/sanic/signals.py
index b5069068..451d0269 100644
--- a/sanic/signals.py
+++ b/sanic/signals.py
@@ -75,6 +75,8 @@ RESERVED_NAMESPACES = {
     },
 }
 
+GENERIC_SIGNAL_FORMAT = "__generic__.__signal__.%s"
+
 
 def _blank():
     ...
@@ -101,6 +103,20 @@ class SignalRouter(BaseRouter):
         self.allow_fail_builtin = True
         self.ctx.loop = None
 
+    @staticmethod
+    def format_event(event: str) -> str:
+        """Ensure event strings in proper format
+
+        Args:
+            event (str): event string
+
+        Returns:
+            str: formatted event string
+        """
+        if "." not in event:
+            event = GENERIC_SIGNAL_FORMAT % event
+        return event
+
     def get(  # type: ignore
         self,
         event: str,
@@ -118,6 +134,7 @@ class SignalRouter(BaseRouter):
         Raises:
             NotFound: If no handlers are found
         """  # noqa: E501
+        event = self.format_event(event)
         extra = condition or {}
         try:
             group, param_basket = self.find_route(
@@ -157,6 +174,7 @@ class SignalRouter(BaseRouter):
         fail_not_found: bool = True,
         reverse: bool = False,
     ) -> Any:
+        event = self.format_event(event)
         try:
             group, handlers, params = self.get(event, condition=condition)
         except NotFound as e:
@@ -236,6 +254,7 @@ class SignalRouter(BaseRouter):
             RuntimeError: If the signal is dispatched outside of an event loop
         """  # noqa: E501
 
+        event = self.format_event(event)
         dispatch = self._dispatch(
             event,
             context=context,
@@ -259,6 +278,7 @@ class SignalRouter(BaseRouter):
         condition: Optional[Dict[str, Any]] = None,
         exclusive: bool = True,
     ) -> Signal:
+        event = self.format_event(event)
         event_definition = event
         parts = self._build_event_parts(event)
         if parts[2].startswith("<"):


Test patch:
diff --git a/tests/test_signals.py b/tests/test_signals.py
index 8a564a98..670a563d 100644
--- a/tests/test_signals.py
+++ b/tests/test_signals.py
@@ -65,7 +65,6 @@ def test_add_signal_decorator(app):
     (
         "<foo>.bar.bax",
         "foo.<bar>.baz",
-        "foo",
         "foo.bar",
         "foo.bar.baz.qux",
     ),
@@ -292,6 +291,54 @@ async def test_dispatch_signal_triggers_event(app):
     assert app_counter == 1
 
 
+@pytest.mark.asyncio
+async def test_dispatch_simple_signal_triggers(app):
+    counter = 0
+
+    @app.signal("foo")
+    def sync_signal():
+        nonlocal counter
+
+        counter += 1
+
+    app.signal_router.finalize()
+
+    await app.dispatch("foo")
+    assert counter == 1
+
+
+@pytest.mark.asyncio
+async def test_dispatch_simple_signal_triggers_dynamic(app):
+    counter = 0
+
+    @app.signal("<foo:int>")
+    def sync_signal(foo):
+        nonlocal counter
+
+        counter += foo
+
+    app.signal_router.finalize()
+
+    await app.dispatch("9")
+    assert counter == 9
+
+
+@pytest.mark.asyncio
+async def test_dispatch_simple_signal_triggers(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_event_on_bp(app):
     bp = Blueprint("bp")