Repository: sanic-org/sanic

Issue:
Missing priority functionality in app-wide middlware
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Describe the bug

In v22.9 the priority feature was added, but unfortunately it appears it was overlooked and there is priority parameter to `register_middleware()` for `Sanic`.

I am looking to use this feature because I have a Blueprint-specific middleware which appears to run before the application-wide middleware. In my case the application-wide middleware does authorization and updates `request.ctx` which is then used in the Blueprint-specific middleware.

### Code snippet

_No response_

### Expected Behavior

_No response_

### How do you run Sanic?

ASGI

### Operating System

Windows

### Sanic Version

Sanic 22.9.1; Routing 22.8.0

### Additional context

_No response_

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/sanic-org/sanic/pull/2636?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) Report
> :exclamation: No coverage uploaded for pull request base (`main@911485d`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#section-missing-base-commit).
> Patch coverage: 100.000% of modified lines in pull request are covered.

> :exclamation: Current head d1e2559 differs from pull request most recent head 5da0c0e. Consider uploading reports for the commit 5da0c0e to get more accurate results

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


```diff
@@           Coverage Diff            @@
##             main     #2636   +/-   ##
========================================
  Coverage        ?   88.589%           
========================================
  Files           ?        82           
  Lines           ?      6757           
  Branches        ?      1154           
========================================
  Hits            ?      5986           
  Misses          ?       531           
  Partials        ?       240           
```


| [Impacted Files](https://codecov.io/gh/sanic-org/sanic/pull/2636?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) | Coverage Δ | |
|---|---|---|
| [sanic/app.py](https://codecov.io/gh/sanic-org/sanic/pull/2636/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvYXBwLnB5) | `89.285% <100.000%> (ø)` | |
| [sanic/middleware.py](https://codecov.io/gh/sanic-org/sanic/pull/2636/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvbWlkZGxld2FyZS5weQ==) | `96.969% <100.000%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/sanic-org/sanic/pull/2636?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](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/app.py b/sanic/app.py
index 8d098663..cbefcc4d 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -61,7 +61,7 @@ from sanic.exceptions import (
     URLBuildError,
 )
 from sanic.handlers import ErrorHandler
-from sanic.helpers import Default
+from sanic.helpers import Default, _default
 from sanic.http import Stage
 from sanic.log import (
     LOGGING_CONFIG_DEFAULTS,
@@ -69,6 +69,7 @@ from sanic.log import (
     error_logger,
     logger,
 )
+from sanic.middleware import Middleware, MiddlewareLocation
 from sanic.mixins.listeners import ListenerEvent
 from sanic.mixins.startup import StartupMixin
 from sanic.models.futures import (
@@ -294,8 +295,12 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
         return listener
 
     def register_middleware(
-        self, middleware: MiddlewareType, attach_to: str = "request"
-    ) -> MiddlewareType:
+        self,
+        middleware: Union[MiddlewareType, Middleware],
+        attach_to: str = "request",
+        *,
+        priority: Union[Default, int] = _default,
+    ) -> Union[MiddlewareType, Middleware]:
         """
         Register an application level middleware that will be attached
         to all the API URLs registered under this application.
@@ -311,19 +316,37 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
             **response** - Invoke before the response is returned back
         :return: decorated method
         """
-        if attach_to == "request":
+        retval = middleware
+        location = MiddlewareLocation[attach_to.upper()]
+
+        if not isinstance(middleware, Middleware):
+            middleware = Middleware(
+                middleware,
+                location=location,
+                priority=priority if isinstance(priority, int) else 0,
+            )
+        elif middleware.priority != priority and isinstance(priority, int):
+            middleware = Middleware(
+                middleware.func,
+                location=middleware.location,
+                priority=priority,
+            )
+
+        if location is MiddlewareLocation.REQUEST:
             if middleware not in self.request_middleware:
                 self.request_middleware.append(middleware)
-        if attach_to == "response":
+        if location is MiddlewareLocation.RESPONSE:
             if middleware not in self.response_middleware:
                 self.response_middleware.appendleft(middleware)
-        return middleware
+        return retval
 
     def register_named_middleware(
         self,
         middleware: MiddlewareType,
         route_names: Iterable[str],
         attach_to: str = "request",
+        *,
+        priority: Union[Default, int] = _default,
     ):
         """
         Method for attaching middleware to specific routes. This is mainly an
@@ -337,19 +360,35 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
             defaults to "request"
         :type attach_to: str, optional
         """
-        if attach_to == "request":
+        retval = middleware
+        location = MiddlewareLocation[attach_to.upper()]
+
+        if not isinstance(middleware, Middleware):
+            middleware = Middleware(
+                middleware,
+                location=location,
+                priority=priority if isinstance(priority, int) else 0,
+            )
+        elif middleware.priority != priority and isinstance(priority, int):
+            middleware = Middleware(
+                middleware.func,
+                location=middleware.location,
+                priority=priority,
+            )
+
+        if location is MiddlewareLocation.REQUEST:
             for _rn in route_names:
                 if _rn not in self.named_request_middleware:
                     self.named_request_middleware[_rn] = deque()
                 if middleware not in self.named_request_middleware[_rn]:
                     self.named_request_middleware[_rn].append(middleware)
-        if attach_to == "response":
+        if location is MiddlewareLocation.RESPONSE:
             for _rn in route_names:
                 if _rn not in self.named_response_middleware:
                     self.named_response_middleware[_rn] = deque()
                 if middleware not in self.named_response_middleware[_rn]:
                     self.named_response_middleware[_rn].appendleft(middleware)
-        return middleware
+        return retval
 
     def _apply_exception_handler(
         self,
diff --git a/sanic/middleware.py b/sanic/middleware.py
index 5bbd777b..0c6058fa 100644
--- a/sanic/middleware.py
+++ b/sanic/middleware.py
@@ -32,6 +32,9 @@ class Middleware:
     def __call__(self, *args, **kwargs):
         return self.func(*args, **kwargs)
 
+    def __hash__(self) -> int:
+        return hash(self.func)
+
     def __repr__(self) -> str:
         return (
             f"{self.__class__.__name__}("


Test patch:
diff --git a/tests/test_middleware_priority.py b/tests/test_middleware_priority.py
index 9646f6d0..c16f658b 100644
--- a/tests/test_middleware_priority.py
+++ b/tests/test_middleware_priority.py
@@ -3,7 +3,7 @@ from functools import partial
 import pytest
 
 from sanic import Sanic
-from sanic.middleware import Middleware
+from sanic.middleware import Middleware, MiddlewareLocation
 from sanic.response import json
 
 
@@ -40,6 +40,86 @@ def reset_middleware():
     Middleware.reset_count()
 
 
+def test_add_register_priority(app: Sanic):
+    def foo(*_):
+        ...
+
+    app.register_middleware(foo, priority=999)
+    assert len(app.request_middleware) == 1
+    assert len(app.response_middleware) == 0
+    assert app.request_middleware[0].priority == 999  # type: ignore
+    app.register_middleware(foo, attach_to="response", priority=999)
+    assert len(app.request_middleware) == 1
+    assert len(app.response_middleware) == 1
+    assert app.response_middleware[0].priority == 999  # type: ignore
+
+
+def test_add_register_named_priority(app: Sanic):
+    def foo(*_):
+        ...
+
+    app.register_named_middleware(foo, route_names=["foo"], priority=999)
+    assert len(app.named_request_middleware) == 1
+    assert len(app.named_response_middleware) == 0
+    assert app.named_request_middleware["foo"][0].priority == 999  # type: ignore
+    app.register_named_middleware(
+        foo, attach_to="response", route_names=["foo"], priority=999
+    )
+    assert len(app.named_request_middleware) == 1
+    assert len(app.named_response_middleware) == 1
+    assert app.named_response_middleware["foo"][0].priority == 999  # type: ignore
+
+
+def test_add_decorator_priority(app: Sanic):
+    def foo(*_):
+        ...
+
+    app.middleware(foo, priority=999)
+    assert len(app.request_middleware) == 1
+    assert len(app.response_middleware) == 0
+    assert app.request_middleware[0].priority == 999  # type: ignore
+    app.middleware(foo, attach_to="response", priority=999)
+    assert len(app.request_middleware) == 1
+    assert len(app.response_middleware) == 1
+    assert app.response_middleware[0].priority == 999  # type: ignore
+
+
+def test_add_convenience_priority(app: Sanic):
+    def foo(*_):
+        ...
+
+    app.on_request(foo, priority=999)
+    assert len(app.request_middleware) == 1
+    assert len(app.response_middleware) == 0
+    assert app.request_middleware[0].priority == 999  # type: ignore
+    app.on_response(foo, priority=999)
+    assert len(app.request_middleware) == 1
+    assert len(app.response_middleware) == 1
+    assert app.response_middleware[0].priority == 999  # type: ignore
+
+
+def test_add_conflicting_priority(app: Sanic):
+    def foo(*_):
+        ...
+
+    middleware = Middleware(foo, MiddlewareLocation.REQUEST, priority=998)
+    app.register_middleware(middleware=middleware, priority=999)
+    assert app.request_middleware[0].priority == 999  # type: ignore
+    middleware.priority == 998
+
+
+def test_add_conflicting_priority_named(app: Sanic):
+    def foo(*_):
+        ...
+
+    middleware = Middleware(foo, MiddlewareLocation.REQUEST, priority=998)
+    app.register_named_middleware(
+        middleware=middleware, route_names=["foo"], priority=999
+    )
+    assert app.named_request_middleware["foo"][0].priority == 999  # type: ignore
+    middleware.priority == 998
+
+
 @pytest.mark.parametrize(
     "expected,priorities",
     PRIORITY_TEST_CASES,