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:
Kludex: Are there other cases we are forgetting?
flxdot: > Are there other cases we are forgetting?

The only `Callable`s I'm aware of are:

* functions
* classes
* methods (incl. static & class methods)
* lambda

But in the nature of the lambda function is anonymous, we can not read a name.

After consulting the docs of the `inspect` module again we could simplify the implementation by using `inspect.routine`:

```python
def get_name(endpoint: typing.Callable) -> str:
    if inspect.isroutine(endpoint):
        return endpoint.__name__
    return endpoint.__class__.__name__
```

This might create issues with the lambda functions though:


```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'})

    @classmethod
    async def my_classmethod(self, request):
        return JSONResponse({'endpoint_type': 'classmethod'})

    @staticmethod
    async def my_staticmethod(self, request):
        return JSONResponse({'endpoint_type': 'staticmethod'})


function_route = Route('/functionEndpoint', my_function)
class_route = Route('/classEndpoint', MyClass())
endpoint_obj = MySpecialEndpointObject()
method_route = Route('/methodEndpoint', endpoint_obj.my_method)
classmethod_route = Route('/classMethodEndpoint', endpoint_obj.my_classmethod)
staticmethod_route = Route('/staticMethodEndpoint', endpoint_obj.my_staticmethod)
lambda_route = Route('/lambdaEndpoint', lambda request: JSONResponse({'endpoint_type': 'lambda'}))

assert function_route.name == "my_function"
assert class_route.name == "MyClass"
assert method_route.name == "my_method"
assert classmethod_route.name == "my_classmethod"
assert staticmethod_route.name == "my_staticmethod"
assert lambda_route.name == "lambda" # AssertionError, because it will be: "<lambda>"
```
adriangb: I think it's okay if lambdas aren't handled correctly 
Kludex: Can we add a test for it?
flxdot: > Can we add a test for it?

I'm currently quite busy. But I'll add tests next week.
Kludex: @flxdot Are you be able to work on this soon or can I take it over?
flxdot: > @flxdot Are you be able to work on this soon or can I take it over?

Friday afternoons seem to be only free time atm.

Added the tests. Since `get_name()` is rather an internal function I decided not to test the function, but to rather test the behaviour of the common public usage (building of routes). If you would rather like to test the function I can quickly change it.
Kludex: Thanks for the tests!

Yeah, no need to test specifically `get_name`. I've added some comments to simplify the tests added, jfyk.
flxdot: Good call reducing the clutter in the test. Thanks for the hint!
Kludex: Once the linter issues are resolved, we can merge it.
flxdot: sorry for all that back and forth. Should have dug deeper into the GitHub action to see, what is run.
Kludex: Thanks @flxdot ! :)

Patch:
diff --git a/starlette/routing.py b/starlette/routing.py
index 0388304..ea6ec21 100644
--- a/starlette/routing.py
+++ b/starlette/routing.py
@@ -84,7 +84,7 @@ def websocket_session(func: typing.Callable) -> ASGIApp:
 
 
 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
index 7077c56..e8adaca 100644
--- 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