Repository: pydantic/pydantic
Pull request: Make sure `inspect.iscoroutinefunction` works on coroutines decorated with `@validate_call`
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->

## Change Summary

Add judgement for validate_call decorator. Now we can get `True` return from `inpect.iscoroutinefunction()`.

## Related issue number

fix #10370 

## Checklist

* [ ] The pull request title is a good summary of the changes - it will be used in the changelog
* [x] Unit tests for the changes exist
* [x] Tests pass on CI
* [ ] Documentation reflects the changes where applicable
* [ ] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


Selected Reviewer: @sydney-runkle

Problem statement:
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 from issue discussion:
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!

Commit message: fix validate_call decorator #10370

Patch to review:
diff --git a/pydantic/validate_call_decorator.py b/pydantic/validate_call_decorator.py
index fef84af912c..480cd1f88dc 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
@@ -59,9 +60,17 @@ def validate(function: AnyCallableT) -> AnyCallableT:
         def wrapper_function(*args, **kwargs):
             return validate_call_wrapper(*args, **kwargs)
 
+        @functools.wraps(function)
+        async def async_wrapper_function(*args, **kwargs):
+            return await validate_call_wrapper(*args, **kwargs)
+
         wrapper_function.raw_function = function  # type: ignore
+        async_wrapper_function.raw_function = function  # type: ignore
 
-        return wrapper_function  # type: ignore
+        if inspect.iscoroutinefunction(function):
+            return async_wrapper_function  # type: ignore
+        else:
+            return wrapper_function  # type: ignore
 
     if func:
         return validate(func)


Code review comments:
diff_hunk: @@ -59,9 +60,17 @@ def validate(function: AnyCallableT) -> AnyCallableT:
         def wrapper_function(*args, **kwargs):
             return validate_call_wrapper(*args, **kwargs)
 
+        @functools.wraps(function)
+        async def async_wrapper_function(*args, **kwargs):
+            return await validate_call_wrapper(*args, **kwargs)
+
         wrapper_function.raw_function = function  # type: ignore
+        async_wrapper_function.raw_function = function  # type: ignore
 
-        return wrapper_function  # type: ignore
+        if inspect.iscoroutinefunction(function):
+            return async_wrapper_function  # type: ignore
+        else:
+            return wrapper_function  # type: ignore
path: pydantic/validate_call_decorator.py
text: @user1:
Let's add the function definitions (`wrapper_function` and `async_wrapper_function`) to the relevant parts of this conditional. Can also move the `.raw_function` assignments there as well.

@author:
ok