Repository: pydantic/pydantic
Issue category: Feature Request

Issue:
Support async functions with `@validate_call` decorator
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

I'm writing a project using `asgiref`.

When I use `async_to_sync`, I got a error ` UserWarning: async_to_sync was passed a non-async-marked callable`.

I think it's due to `@validate_call` decorator because it's not appear if i delete `@validate_call`.

For example, here is a simple code. The correct output is `True` but actual output is `False`.  How can we fix this bug?

### Example Code

```Python
import asyncio
import inspect

from pydantic import validate_call


@validate_call(validate_return=True)
async def delay_number(n: int) -> int:
    await asyncio.sleep(1)
    return n


if __name__ == "__main__":
    print(inspect.iscoroutinefunction(delay_number))
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.9.1
        pydantic-core version: 2.23.3
          pydantic-core build: profile=release pgo=false
                 install path: C:\Users\movis\miniconda3\envs\volans_env\Lib\site-packages\pydantic
               python version: 3.11.9 | packaged by Anaconda, Inc. | (main, Apr 19 2024, 16:40:41) [MSC v.1916 64 bit (AMD64)]
                     platform: Windows-10-10.0.22631-SP0
             related packages: fastapi-0.112.4 typing_extensions-4.12.2
                       commit: unknown
```



Hints:
Maybe `pydantic._internal._validate_call.ValidateCallWrapper` need some changes to adapt async function(coroutine)?
Yeah. I don't think it's likely that we'll support async functions in the short term, but we can leave this open as a potential feature.
> Yeah. I don't think it's likely that we'll support async functions in the short term, but we can leave this open as a potential feature.

Thanks. In fact, the async function in this example works just fine in asynchronous way. This is also mentioned in the documentation: https://docs.pydantic.dev/2.9/concepts/validation_decorator/#async-functions

 It looks like the cpython interpreter knows that is a coroutine, but `inspect.iscoroutinefunction` doesn't! So I think it's just a problem of recognizing, I tried to override the decorator, and get correct answer `True` from `inspect.iscoroutinefunction`.

Raw Decorator(pydantic.validate_call_decorator.py):

```python
from __future__ import annotations as _annotations

import functools
from typing import TYPE_CHECKING, Any, Callable, TypeVar, overload

from ._internal import _typing_extra, _validate_call

__all__ = ('validate_call',)

if TYPE_CHECKING:
    from .config import ConfigDict

    AnyCallableT = TypeVar('AnyCallableT', bound=Callable[..., Any])


def validate_call(
    func: AnyCallableT | None = None,
    /,
    *,
    config: ConfigDict | None = None,
    validate_return: bool = False,
) -> AnyCallableT | Callable[[AnyCallableT], AnyCallableT]:
    local_ns = _typing_extra.parent_frame_namespace()

    def validate(function: AnyCallableT) -> AnyCallableT:
        if isinstance(function, (classmethod, staticmethod)):
            name = type(function).__name__
            raise TypeError(f'The `@{name}` decorator should be applied after `@validate_call` (put `@{name}` on top)')

        validate_call_wrapper = _validate_call.ValidateCallWrapper(function, config, validate_return, local_ns)

        @functools.wraps(function)
        def wrapper_function(*args, **kwargs):
            return validate_call_wrapper(*args, **kwargs)

        wrapper_function.raw_function = function  # type: ignore

        return wrapper_function  # type: ignore

    if func:
        return validate(func)
    else:
        return validate
```

New Decorator(add judgment on async functions in decorator):

```python
from __future__ import annotations as _annotations

import functools
import inspect
from typing import TYPE_CHECKING, Any, Callable, TypeVar, overload

from pydantic._internal import _typing_extra, _validate_call

__all__ = ("validate_call",)

if TYPE_CHECKING:
    from pydantic.config import ConfigDict

    AnyCallableT = TypeVar("AnyCallableT", bound=Callable[..., Any])


def validate_call(
    func: AnyCallableT | None = None,
    /,
    *,
    config: ConfigDict | None = None,
    validate_return: bool = False,
) -> AnyCallableT | Callable[[AnyCallableT], AnyCallableT]:
    local_ns = _typing_extra.parent_frame_namespace()

    def validate(function: AnyCallableT) -> AnyCallableT:
        if isinstance(function, (classmethod, staticmethod)):
            name = type(function).__name__
            raise TypeError(
                f"The `@{name}` decorator should be applied after `@validate_call` (put `@{name}` on top)"
            )

        validate_call_wrapper = _validate_call.ValidateCallWrapper(
            function, config, validate_return, local_ns
        )

        @functools.wraps(function)
        def wrapper_function(*args, **kwargs):
            return validate_call_wrapper(*args, **kwargs)

        @functools.wraps(function)
        async def async_wrapper_function(*args, **kwargs):
            return validate_call_wrapper(*args, **kwargs)

        wrapper_function.raw_function = function  # type: ignore

        if inspect.iscoroutinefunction(function):
            return async_wrapper_function
        else:
            return wrapper_function  # type: ignore

    if func:
        return validate(func)
    else:
        return validate
```

So do you think it's right?(Thanks for your reply)

By the way, it's related to https://github.com/python/cpython/issues/100317
Huh, I forgot that we offered support! That's great, thanks for the ref!

I'd say we probably want to have the functions defined within this conditional:

```
if inspect.iscoroutinefunction(function):
    return async_wrapper_function
else:
    return wrapper_function  # type: ignore
```

Other than that, looks good as a first pass to me, feel free to open a PR and I can review!

Patch:
diff --git a/pydantic/validate_call_decorator.py b/pydantic/validate_call_decorator.py
index fef84af912..509a7463c5 100644
--- a/pydantic/validate_call_decorator.py
+++ b/pydantic/validate_call_decorator.py
@@ -3,6 +3,7 @@
 from __future__ import annotations as _annotations
 
 import functools
+import inspect
 from typing import TYPE_CHECKING, Any, Callable, TypeVar, overload
 
 from ._internal import _typing_extra, _validate_call
@@ -55,12 +56,18 @@ def validate(function: AnyCallableT) -> AnyCallableT:
 
         validate_call_wrapper = _validate_call.ValidateCallWrapper(function, config, validate_return, local_ns)
 
-        @functools.wraps(function)
-        def wrapper_function(*args, **kwargs):
-            return validate_call_wrapper(*args, **kwargs)
+        if inspect.iscoroutinefunction(function):
 
-        wrapper_function.raw_function = function  # type: ignore
+            @functools.wraps(function)
+            async def wrapper_function(*args, **kwargs):  # type: ignore
+                return await validate_call_wrapper(*args, **kwargs)
+        else:
+
+            @functools.wraps(function)
+            def wrapper_function(*args, **kwargs):
+                return validate_call_wrapper(*args, **kwargs)
 
+        wrapper_function.raw_function = function  # type: ignore
         return wrapper_function  # type: ignore
 
     if func:


Test patch:
diff --git a/tests/test_validate_call.py b/tests/test_validate_call.py
index 12547f5a4f..e0a10504c9 100644
--- a/tests/test_validate_call.py
+++ b/tests/test_validate_call.py
@@ -292,6 +292,9 @@ async def run():
         v = await foo(1, 2)
         assert v == 'a=1 b=2'
 
+    # insert_assert(inspect.iscoroutinefunction(foo) is True)
+    assert inspect.iscoroutinefunction(foo) is True
+
     asyncio.run(run())
     with pytest.raises(ValidationError) as exc_info:
         asyncio.run(foo('x'))