Repository: encode/starlette

Issue:
Route naming introspection always return "method" for method endpoints 
Discussion was done at https://gitter.im/encode/community. Bug was confirmed by @Kludex.

## Description
methods don't get detected on `is_function`, then we assume that `<object>.__class__.__name__ ` will give the right name (my_method on the example below) for it, but it actually gets the "method" name, which is wrong.

Unexpected behaviour seem to originate from: https://github.com/encode/starlette/blob/e086fc2da361767b532cf690e5203619bbae98aa/starlette/routing.py#L87

## Minimal example
```python
from starlette.responses import JSONResponse
from starlette.routing import Route


async def my_function(request):
    return JSONResponse({'endpoint_type': 'function'})


class MyClass:
    def __call__(self, request):
        return JSONResponse({'endpoint_type': 'class'})


class MySpecialEndpointObject:
    async def my_method(self, request):
        return JSONResponse({'endpoint_type': 'method'})


endpoint_obj = MySpecialEndpointObject()
function_route = Route('/functionEndpoint', my_function)
class_route = Route('/classEndpoint', MyClass())
method_route = Route('/methodEndpoint', endpoint_obj.my_method)

assert function_route.name == "my_function"
assert class_route.name == "MyClass"
assert method_route.name == "my_method"  # AssertionError
```

## Actual behavior

Value of `method_route.name` is `"method"`.

## Expected behavior

Value of `method_route.name` is `"my_method"`.

It could also be `"MySpecialEndpointObject_my_method"`. Reason here is to prevent ambiguity.


Discussion:

Resolving patch:
diff --git a/starlette/routing.py b/starlette/routing.py
--- a/starlette/routing.py
+++ b/starlette/routing.py
@@ -84,7 +84,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None:
 
 
 def get_name(endpoint: typing.Callable) -> str:
-    if inspect.isfunction(endpoint) or inspect.isclass(endpoint):
+    if inspect.isroutine(endpoint) or inspect.isclass(endpoint):
         return endpoint.__name__
     return endpoint.__class__.__name__
 


Test patch:
diff --git a/tests/test_routing.py b/tests/test_routing.py
--- a/tests/test_routing.py
+++ b/tests/test_routing.py
@@ -1,4 +1,5 @@
 import functools
+import typing
 import uuid
 
 import pytest
@@ -710,3 +711,38 @@ def test_duplicated_param_names():
         match="Duplicated param names id, name at path /{id}/{name}/{id}/{name}",
     ):
         Route("/{id}/{name}/{id}/{name}", user)
+
+
+class Endpoint:
+    async def my_method(self, request):
+        ...  # pragma: no cover
+
+    @classmethod
+    async def my_classmethod(cls, request):
+        ...  # pragma: no cover
+
+    @staticmethod
+    async def my_staticmethod(request):
+        ...  # pragma: no cover
+
+    def __call__(self, request):
+        ...  # pragma: no cover
+
+
+@pytest.mark.parametrize(
+    "endpoint, expected_name",
+    [
+        pytest.param(func_homepage, "func_homepage", id="function"),
+        pytest.param(Endpoint().my_method, "my_method", id="method"),
+        pytest.param(Endpoint.my_classmethod, "my_classmethod", id="classmethod"),
+        pytest.param(
+            Endpoint.my_staticmethod,
+            "my_staticmethod",
+            id="staticmethod",
+        ),
+        pytest.param(Endpoint(), "Endpoint", id="object"),
+        pytest.param(lambda request: ..., "<lambda>", id="lambda"),
+    ],
+)
+def test_route_name(endpoint: typing.Callable, expected_name: str):
+    assert Route(path="/", endpoint=endpoint).name == expected_name