Repository: huge-success/sanic
Pull request #1972 (2020-11-22 23:10:35)

Name endpoints at startup
I was messing around with `Sanic.handle_request` and i realized there was this fairly simple optimization to be made. There is no reason that we calculate the `request.endpoint` at runtime. This can be at startup and cached on the `Route` object.

After looking closer at _how_ this is working, it is a prime example for further refactoring in the future because I do not think that it is doing a good job in handling names of nested objects (app, blueprints, CBV, etc). I have some different methods on how this can be achieved, but I want to hold off on adding it until after the first of the year.

In total, this is a fairly minor change.

ahopkins: Here are some benchmarks before/after. Take them as you will. I do not claim them to be scientifically accurate representation of this change.


```
Running 30s test @ http://127.0.0.1:3000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     4.40ms    5.02ms 100.84ms   89.17%
    Req/Sec     9.94k     2.08k   25.15k    73.10%
  3561967 requests in 30.10s, 400.84MB read
Requests/sec: 118357.37
Transfer/sec:     13.32MB

Running 30s test @ http://127.0.0.1:3000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     4.25ms    4.93ms 117.93ms   90.01%
    Req/Sec    10.16k     2.60k   26.17k    74.75%
  3640517 requests in 30.05s, 409.68MB read
Requests/sec: 121137.64
Transfer/sec:     13.63MB
```
codecov[bot]: # [Codecov](https://codecov.io/gh/huge-success/sanic/pull/1972?src=pr&el=h1) Report
> Merging [#1972](https://codecov.io/gh/huge-success/sanic/pull/1972?src=pr&el=desc) (831b472) into [master](https://codecov.io/gh/huge-success/sanic/commit/63567c2ae45086989aef5b33c969464c0de2442e?el=desc) (63567c2) will **increase** coverage by `0.18%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/huge-success/sanic/pull/1972/graphs/tree.svg?width=650&height=150&src=pr&token=NqLbJHUqoR)](https://codecov.io/gh/huge-success/sanic/pull/1972?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1972      +/-   ##
==========================================
+ Coverage   92.33%   92.52%   +0.18%     
==========================================
  Files          29       29              
  Lines        3263     3264       +1     
  Branches      574      573       -1     
==========================================
+ Hits         3013     3020       +7     
+ Misses        170      166       -4     
+ Partials       80       78       -2     
```


| [Impacted Files](https://codecov.io/gh/huge-success/sanic/pull/1972?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [sanic/app.py](https://codecov.io/gh/huge-success/sanic/pull/1972/diff?src=pr&el=tree#diff-c2FuaWMvYXBwLnB5) | `93.50% <100.00%> (-0.03%)` | :arrow_down: |
| [sanic/router.py](https://codecov.io/gh/huge-success/sanic/pull/1972/diff?src=pr&el=tree#diff-c2FuaWMvcm91dGVyLnB5) | `97.10% <100.00%> (+0.02%)` | :arrow_up: |
| [sanic/views.py](https://codecov.io/gh/huge-success/sanic/pull/1972/diff?src=pr&el=tree#diff-c2FuaWMvdmlld3MucHk=) | `100.00% <100.00%> (ø)` | |
| [sanic/server.py](https://codecov.io/gh/huge-success/sanic/pull/1972/diff?src=pr&el=tree#diff-c2FuaWMvc2VydmVyLnB5) | `82.17% <0.00%> (+1.12%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/huge-success/sanic/pull/1972?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/huge-success/sanic/pull/1972?src=pr&el=footer). Last update [63567c2...831b472](https://codecov.io/gh/huge-success/sanic/pull/1972?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/sanic/app.py b/sanic/app.py
index e8cb5c17..047879ca 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -68,7 +68,7 @@ class Sanic:
 
         self.name = name
         self.asgi = False
-        self.router = router or Router()
+        self.router = router or Router(self)
         self.request_class = request_class
         self.error_handler = error_handler or ErrorHandler()
         self.config = Config(load_env=load_env)
@@ -900,7 +900,9 @@ class Sanic:
         name = None
         try:
             # Fetch handler from router
-            handler, args, kwargs, uri, name = self.router.get(request)
+            handler, args, kwargs, uri, name, endpoint = self.router.get(
+                request
+            )
 
             # -------------------------------------------- #
             # Request Middleware
@@ -922,16 +924,8 @@ class Sanic:
                             "handler from the router"
                         )
                     )
-                else:
-                    if not getattr(handler, "__blueprintname__", False):
-                        request.endpoint = self._build_endpoint_name(
-                            handler.__name__
-                        )
-                    else:
-                        request.endpoint = self._build_endpoint_name(
-                            getattr(handler, "__blueprintname__", ""),
-                            handler.__name__,
-                        )
+
+                request.endpoint = endpoint
 
                 # Run response handler
                 response = handler(request, *args, **kwargs)
diff --git a/sanic/router.py b/sanic/router.py
index a608f1a2..ffd98f15 100644
--- a/sanic/router.py
+++ b/sanic/router.py
@@ -11,7 +11,16 @@ from sanic.views import CompositionView
 
 
 Route = namedtuple(
-    "Route", ["handler", "methods", "pattern", "parameters", "name", "uri"]
+    "Route",
+    [
+        "handler",
+        "methods",
+        "pattern",
+        "parameters",
+        "name",
+        "uri",
+        "endpoint",
+    ],
 )
 Parameter = namedtuple("Parameter", ["name", "cast"])
 
@@ -79,7 +88,8 @@ class Router:
     routes_always_check = None
     parameter_pattern = re.compile(r"<(.+?)>")
 
-    def __init__(self):
+    def __init__(self, app):
+        self.app = app
         self.routes_all = {}
         self.routes_names = {}
         self.routes_static_files = {}
@@ -299,11 +309,15 @@ class Router:
 
             handler_name = f"{bp_name}.{name or handler.__name__}"
         else:
-            handler_name = name or getattr(handler, "__name__", None)
+            handler_name = name or getattr(
+                handler, "__name__", handler.__class__.__name__
+            )
 
         if route:
             route = merge_route(route, methods, handler)
         else:
+            endpoint = self.app._build_endpoint_name(handler_name)
+
             route = Route(
                 handler=handler,
                 methods=methods,
@@ -311,6 +325,7 @@ class Router:
                 parameters=parameters,
                 name=handler_name,
                 uri=uri,
+                endpoint=endpoint,
             )
 
         self.routes_all[uri] = route
@@ -449,7 +464,8 @@ class Router:
         route_handler = route.handler
         if hasattr(route_handler, "handlers"):
             route_handler = route_handler.handlers[method]
-        return route_handler, [], kwargs, route.uri, route.name
+
+        return route_handler, [], kwargs, route.uri, route.name, route.endpoint
 
     def is_stream_handler(self, request):
         """Handler for request is stream or not.
diff --git a/sanic/views.py b/sanic/views.py
index 30d0abcb..97ca6222 100644
--- a/sanic/views.py
+++ b/sanic/views.py
@@ -90,6 +90,7 @@ class CompositionView:
 
     def __init__(self):
         self.handlers = {}
+        self.name = self.__class__.__name__
 
     def add(self, methods, handler, stream=False):
         if stream:


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 5c217cdd..45963863 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -95,10 +95,10 @@ class RouteStringGenerator:
 
 
 @pytest.fixture(scope="function")
-def sanic_router():
+def sanic_router(app):
     # noinspection PyProtectedMember
     def _setup(route_details: tuple) -> (Router, tuple):
-        router = Router()
+        router = Router(app)
         added_router = []
         for method, route in route_details:
             try:
diff --git a/tests/test_app.py b/tests/test_app.py
index c9c44c39..ad85c3c2 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -117,7 +117,7 @@ def test_app_route_raise_value_error(app):
 
 def test_app_handle_request_handler_is_none(app, monkeypatch):
     def mockreturn(*args, **kwargs):
-        return None, [], {}, "", ""
+        return None, [], {}, "", "", None
 
     # Not sure how to make app.router.get() return None, so use mock here.
     monkeypatch.setattr(app.router, "get", mockreturn)