Repository: litestar-org/litestar

Issue:
Default name in handler index is not unique
**Describe the bug**
Default name for route handlers might be not unique.

[Here](https://github.com/starlite-api/starlite/pull/541#discussion_r987565608) we decided to use `handler.fn.__qualname__` as default name in handler index but today I learned the hard way that `__qualname__` is not unique if your route handlers are functions with the same name located in different files. Sorry, I didn't check python docs carefully to make sure that picked named would be unique.

Easy repro
```
/tmp/test 
> cat a.py 
def create():
    pass

/tmp/test 
> cat b.py 
def create():
    pass

/tmp/test 
> python
Python 3.10.7 (main, Sep  6 2022, 21:22:27) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from a import create as create_a
>>> from b import create as create_b
>>> create_a.__qualname__
'create'
>>> create_b.__qualname__
'create'
>>> 
```

Django uses `func.__module__ + "." + func.__qualname__' for similar purposes as can be see [here](https://github.com/django/django/blob/974942a75039ba43e618f6a5ff95e08b5d5176fd/django/contrib/admindocs/utils.py#L22)

I tried it https://github.com/starlite-api/starlite/compare/main...jtraub:starlite:fix_default_name_in_index and it works fine for my case.
Maybe "internal" method `app._get_handler_qualname` should be moved to utils so the user can import and use it - at the moment I ha to duplicate the code in my app 


Discussion:
yup

Resolving patch:
diff --git a/starlite/app.py b/starlite/app.py
--- a/starlite/app.py
+++ b/starlite/app.py
@@ -101,8 +101,8 @@ class HandlerIndex(TypedDict):
     """Full route paths to the route handler."""
     handler: "RouteHandlerType"
     """Route handler instance."""
-    qualname: str
-    """Qualname of the handler"""
+    identifier: str
+    """Unique identifier of the handler. Either equal to the 'name' attribute or the __str__ value of the handler."""
 
 
 class HandlerNode(TypedDict):
@@ -467,12 +467,11 @@ def handler() -> None:
         if not handler:
             return None
 
-        qualname = self._get_handler_qualname(handler)
-
-        routes = self._route_mapping[qualname]
+        identifier = handler.name or str(handler)
+        routes = self._route_mapping[identifier]
         paths = sorted(unique([route.path for route in routes]))
 
-        return HandlerIndex(handler=handler, paths=paths, qualname=qualname)
+        return HandlerIndex(handler=handler, paths=paths, identifier=identifier)
 
     def route_reverse(self, name: str, **path_parameters: Any) -> Optional[str]:
         """Receives a route handler name, path parameter values and returns an
@@ -483,7 +482,7 @@ def route_reverse(self, name: str, **path_parameters: Any) -> Optional[str]:
             from starlite import Starlite, get
 
 
-            @get("/group/{group_id:int}/user/{user_id:int}", name="get_memebership_details")
+            @get("/group/{group_id:int}/user/{user_id:int}", name="get_membership_details")
             def get_membership_details(group_id: int, user_id: int) -> None:
                 pass
 
@@ -512,7 +511,7 @@ def get_membership_details(group_id: int, user_id: int) -> None:
         output: List[str] = []
 
         routes = sorted(
-            self._route_mapping[handler_index["qualname"]], key=lambda r: len(r.path_parameters), reverse=True
+            self._route_mapping[handler_index["identifier"]], key=lambda r: len(r.path_parameters), reverse=True
         )
         passed_parameters = set(path_parameters.keys())
 
@@ -614,7 +613,8 @@ def _add_node_to_route_map(self, route: BaseRoute) -> RouteMapNode:
         self._configure_route_map_node(route, current_node)
         return current_node
 
-    def _get_route_handlers(self, route: BaseRoute) -> List["RouteHandlerType"]:
+    @staticmethod
+    def _get_route_handlers(route: BaseRoute) -> List["RouteHandlerType"]:
         """Retrieve handler(s) as a list for given route."""
         route_handlers: List["RouteHandlerType"] = []
         if isinstance(route, (WebSocketRoute, ASGIRoute)):
@@ -624,14 +624,6 @@ def _get_route_handlers(self, route: BaseRoute) -> List["RouteHandlerType"]:
 
         return route_handlers
 
-    def _get_handler_qualname(self, handler: "RouteHandlerType") -> str:
-        """Retrieves __qualname__ for passed route handler."""
-        handler_fn = cast("AnyCallable", handler.fn)
-        if hasattr(handler_fn, "__qualname__"):
-            return handler_fn.__qualname__
-
-        return type(handler_fn).__qualname__
-
     def _store_handler_to_route_mapping(self, route: BaseRoute) -> None:
         """Stores the mapping of route handlers to routes and to route handler
         names.
@@ -645,18 +637,15 @@ def _store_handler_to_route_mapping(self, route: BaseRoute) -> None:
         route_handlers = self._get_route_handlers(route)
 
         for handler in route_handlers:
-            qualname = self._get_handler_qualname(handler)
-            self._route_mapping[qualname].append(route)
-
-            name = handler.name or self._get_handler_qualname(handler)
-
-            if (
-                name in self._route_handler_index
-                and self._get_handler_qualname(self._route_handler_index[name]) != qualname
+            if handler.name in self._route_handler_index and str(self._route_handler_index[handler.name]) != str(
+                handler
             ):
-                raise ImproperlyConfiguredException(f"route handler names must be unique - {name} is not unique.")
-
-            self._route_handler_index[name] = handler
+                raise ImproperlyConfiguredException(
+                    f"route handler names must be unique - {handler.name} is not unique."
+                )
+            identifier = handler.name or str(handler)
+            self._route_mapping[identifier].append(route)
+            self._route_handler_index[identifier] = handler
 
     def _configure_route_map_node(self, route: BaseRoute, node: RouteMapNode) -> None:
         """Set required attributes and route handlers on route_map tree
diff --git a/starlite/handlers/base.py b/starlite/handlers/base.py
--- a/starlite/handlers/base.py
+++ b/starlite/handlers/base.py
@@ -210,3 +210,13 @@ def _validate_handler_function(self) -> None:
         return annotations."""
         if not self.fn:
             raise ImproperlyConfiguredException("Cannot call _validate_handler_function without first setting self.fn")
+
+    def __str__(self) -> str:
+        """
+        Returns:
+            A unique identifier for the route handler
+        """
+        target = cast("Any", self.fn)
+        if not hasattr(target, "__qualname__"):
+            target = type(target)
+        return f"{target.__module__}.{target.__qualname__}"
diff --git a/starlite/utils/exception.py b/starlite/utils/exception.py
--- a/starlite/utils/exception.py
+++ b/starlite/utils/exception.py
@@ -29,7 +29,7 @@ def get_exception_handler(
 
     Args:
         exception_handlers: Mapping of status codes and exception types to handlers.
-    exc: Exception Instance to be resolved to a handler.
+        exc: Exception Instance to be resolved to a handler.
 
     Returns:
         Optional exception handler callable.


Test patch:
diff --git a/tests/kwargs/test_multipart_data.py b/tests/kwargs/test_multipart_data.py
--- a/tests/kwargs/test_multipart_data.py
+++ b/tests/kwargs/test_multipart_data.py
@@ -238,7 +238,7 @@ def test_multipart_request_mixed_files_and_data() -> None:
                 b"value1\r\n"
                 b"--a7f7ac8d4e2e437c877bb7b8d7cc549c--\r\n"
             ),
-            headers={"Content-Type": ("multipart/form-data; boundary=a7f7ac8d4e2e437c877bb7b8d7cc549c")},
+            headers={"Content-Type": "multipart/form-data; boundary=a7f7ac8d4e2e437c877bb7b8d7cc549c"},
         )
         assert response.json() == {
             "file": {
@@ -263,7 +263,7 @@ def test_multipart_request_with_charset_for_filename() -> None:
                 b"<file content>\r\n"
                 b"--a7f7ac8d4e2e437c877bb7b8d7cc549c--\r\n"
             ),
-            headers={"Content-Type": ("multipart/form-data; charset=utf-8; boundary=a7f7ac8d4e2e437c877bb7b8d7cc549c")},
+            headers={"Content-Type": "multipart/form-data; charset=utf-8; boundary=a7f7ac8d4e2e437c877bb7b8d7cc549c"},
         )
         assert response.json() == {
             "file": {
@@ -286,7 +286,7 @@ def test_multipart_request_without_charset_for_filename() -> None:
                 b"<file content>\r\n"
                 b"--a7f7ac8d4e2e437c877bb7b8d7cc549c--\r\n"
             ),
-            headers={"Content-Type": ("multipart/form-data; boundary=a7f7ac8d4e2e437c877bb7b8d7cc549c")},
+            headers={"Content-Type": "multipart/form-data; boundary=a7f7ac8d4e2e437c877bb7b8d7cc549c"},
         )
         assert response.json() == {
             "file": {
@@ -308,7 +308,7 @@ def test_multipart_request_with_encoded_value() -> None:
                 b"Transf\xc3\xa9rer\r\n"
                 b"--20b303e711c4ab8c443184ac833ab00f--\r\n"
             ),
-            headers={"Content-Type": ("multipart/form-data; charset=utf-8; boundary=20b303e711c4ab8c443184ac833ab00f")},
+            headers={"Content-Type": "multipart/form-data; charset=utf-8; boundary=20b303e711c4ab8c443184ac833ab00f"},
         )
         assert response.json() == {"value": "Transférer"}
 
diff --git a/tests/routing/test_route_indexing.py b/tests/routing/test_route_indexing.py
--- a/tests/routing/test_route_indexing.py
+++ b/tests/routing/test_route_indexing.py
@@ -73,20 +73,18 @@ def handler(self) -> None:
     router = Router("router/", route_handlers=[handler, named_handler, MyController])
     app = Starlite(route_handlers=[router])
 
-    handler_index = app.get_handler_index_by_name(handler.fn.__qualname__)  # type: ignore
+    handler_index = app.get_handler_index_by_name(handler.name or str(handler))
     assert handler_index
     assert handler_index["paths"] == ["/router/handler"]
     assert handler_index["handler"] == handler
-    assert handler_index["qualname"] == handler.fn.__qualname__  # type: ignore
+    assert handler_index["identifier"] == handler.name or str(handler)
 
-    handler_index = app.get_handler_index_by_name(MyController.handler.fn.__qualname__)  # type: ignore
+    handler_index = app.get_handler_index_by_name(MyController.handler.name or str(MyController.handler))
     assert handler_index
     assert handler_index["paths"] == ["/router/test"]
-    # we can not do an assertion on handlers in Controller subclasses
-    assert handler_index["qualname"] == MyController.handler.fn.__qualname__  # type: ignore
+    assert handler_index["identifier"] == MyController.handler.name or str(MyController.handler)
 
-    # test that passing route name overrides default name completely
-    handler_index = app.get_handler_index_by_name(named_handler.fn.__qualname__)  # type: ignore
+    handler_index = app.get_handler_index_by_name(named_handler)  # type: ignore
     assert handler_index is None
 
 
diff --git a/tests/template/test_template.py b/tests/template/test_template.py
--- a/tests/template/test_template.py
+++ b/tests/template/test_template.py
@@ -44,11 +44,11 @@ def test_template_response_receives_request_in_context(template_dir: Path) -> No
     Path(template_dir / "abc.html").write_text("")
 
     @get(path="/")
-    def hanler() -> Template:
+    def handler() -> Template:
         return Template(name="abc.html", context={})
 
     with patch("starlite.response.TemplateResponse") as template_response_mock, create_test_client(
-        route_handlers=[hanler],
+        route_handlers=[handler],
         template_config=TemplateConfig(
             directory=template_dir,
             engine=JinjaTemplateEngine,
@@ -64,15 +64,15 @@ def hanler() -> Template:
         assert isinstance(request, Request)
 
 
-def test_request_can_not_be_overriden_in_context(template_dir: Path) -> None:
+def test_request_can_not_be_overridden_in_context(template_dir: Path) -> None:
     Path(template_dir / "abc.html").write_text("")
 
     @get(path="/")
-    def hanler() -> Template:
+    def handler() -> Template:
         return Template(name="abc.html", context={"request": 123})
 
     with patch("starlite.response.TemplateResponse") as template_response_mock, create_test_client(
-        route_handlers=[hanler],
+        route_handlers=[handler],
         template_config=TemplateConfig(
             directory=template_dir,
             engine=JinjaTemplateEngine,
diff --git a/tests/test_router_registration.py b/tests/test_router_registration.py
--- a/tests/test_router_registration.py
+++ b/tests/test_router_registration.py
@@ -142,23 +142,23 @@ def _handler() -> None:
 
     app = Starlite(route_handlers=[first_router, second_router, handler])
 
-    assert app.route_handler_method_view[handler.fn.__qualname__] == ["/root"]  # type: ignore
-    assert app.route_handler_method_view[MyController.get_method.fn.__qualname__] == [  # type: ignore
+    assert app.route_handler_method_view[str(handler)] == ["/root"]
+    assert app.route_handler_method_view[str(MyController.get_method)] == [
         "/first/test",
         "/second/test",
     ]
 
-    assert app.route_handler_method_view[MyController.ws.fn.__qualname__] == [  # type: ignore
+    assert app.route_handler_method_view[str(MyController.ws)] == [
         "/first/test/socket",
         "/second/test/socket",
     ]
-    assert app.route_handler_method_view[put_handler.fn.__qualname__] == [  # type: ignore
+    assert app.route_handler_method_view[str(put_handler)] == [
         "/first/send",
         "/first/modify",
         "/second/send",
         "/second/modify",
     ]
-    assert app.route_handler_method_view[put_handler.fn.__qualname__] == [  # type: ignore
+    assert app.route_handler_method_view[str(post_handler)] == [
         "/first/send",
         "/first/modify",
         "/second/send",