diff --git a/starlette/routing.py b/starlette/routing.py
index ce5e4d1..1e6ae0b 100644
--- a/starlette/routing.py
+++ b/starlette/routing.py
@@ -2,7 +2,6 @@ import asyncio
 import functools
 import inspect
 import re
-import sys
 import traceback
 import typing
 from enum import Enum
@@ -33,11 +32,10 @@ class Match(Enum):
 def iscoroutinefunction_or_partial(obj: typing.Any) -> bool:
     """
     Correctly determines if an object is a coroutine function,
-    with a fix for partials on Python < 3.8.
+    including those wrapped in functools.partial objects.
     """
-    if sys.version_info < (3, 8):  # pragma: no cover
-        while isinstance(obj, functools.partial):
-            obj = obj.func
+    while isinstance(obj, functools.partial):
+        obj = obj.func
     return inspect.iscoroutinefunction(obj)
 
 

diff --git a/tests/test_routing.py b/tests/test_routing.py
index 27640ef..8927c60 100644
--- a/tests/test_routing.py
+++ b/tests/test_routing.py
@@ -590,16 +590,35 @@ def test_raise_on_shutdown():
             pass  # pragma: nocover
 
 
+class AsyncEndpointClassMethod:
+    @classmethod
+    async def async_endpoint(cls, arg, request):
+        return JSONResponse({"arg": arg})
+
+
 async def _partial_async_endpoint(arg, request):
     return JSONResponse({"arg": arg})
 
 
 partial_async_endpoint = functools.partial(_partial_async_endpoint, "foo")
+partial_cls_async_endpoint = functools.partial(
+    AsyncEndpointClassMethod.async_endpoint, "foo"
+)
 
-partial_async_app = Router(routes=[Route("/", partial_async_endpoint)])
+partial_async_app = Router(
+    routes=[
+        Route("/", partial_async_endpoint),
+        Route("/cls", partial_cls_async_endpoint),
+    ]
+)
 
 
 def test_partial_async_endpoint():
-    response = TestClient(partial_async_app).get("/")
+    test_client = TestClient(partial_async_app)
+    response = test_client.get("/")
     assert response.status_code == 200
     assert response.json() == {"arg": "foo"}
+
+    cls_method_response = test_client.get("/cls")
+    assert cls_method_response.status_code == 200
+    assert cls_method_response.json() == {"arg": "foo"}

Asynchronous classmethod handlers not recognized as coroutines if wrapped in functools.partial
https://github.com/encode/starlette/blob/e4307065ea6dbe708fba9643d14fe7adfff06d46/starlette/routing.py#L38

Although the recent 0.14.0 version adds support for functools.partial for Python 3.6 and Python 3.7, that support appears to be incomplete, not working for classmethods in Python 3.8.

I was unaware of the fact that `inspect.iscoroutinefunction` does not properly detect a functools.partial as async if it wraps an async classmethod.

TLDR: the previous PR was too cautious, we can always unwrap the functools.partial object to be sure we check the underlying object.
tests/test_routing.py::test_partial_async_endpoint
tests/test_routing.py::test_router tests/test_routing.py::test_route_converters tests/test_routing.py::test_url_path_for tests/test_routing.py::test_url_for tests/test_routing.py::test_router_add_route tests/test_routing.py::test_router_duplicate_path tests/test_routing.py::test_router_add_websocket_route tests/test_routing.py::test_protocol_switch tests/test_routing.py::test_mount_urls tests/test_routing.py::test_reverse_mount_urls tests/test_routing.py::test_mount_at_root tests/test_routing.py::test_host_routing tests/test_routing.py::test_host_reverse_urls tests/test_routing.py::test_subdomain_routing tests/test_routing.py::test_subdomain_reverse_urls tests/test_routing.py::test_url_for_with_root_path tests/test_routing.py::test_url_for_with_double_mount tests/test_routing.py::test_standalone_route_matches tests/test_routing.py::test_standalone_route_does_not_match tests/test_routing.py::test_standalone_ws_route_matches tests/test_routing.py::test_standalone_ws_route_does_not_match tests/test_routing.py::test_lifespan_async tests/test_routing.py::test_lifespan_sync tests/test_routing.py::test_raise_on_startup tests/test_routing.py::test_raise_on_shutdown