Repository: pydantic/pydantic
Pull request: Add two overload variants to `validate_arguments`
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->
<!-- See https://pydantic-docs.helpmanual.io/contributing/ for help on Contributing -->

## Change Summary

Added two overload variants to `validate_arguments` for the nested decorator signature.

## Related issue number

fix #2052

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] ~~Documentation reflects the changes where applicable~~
* [x] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)


Problem statement:
mypy "error: <nothing> not callable" when passing config to validate_arguments
# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.7
            pydantic compiled: True
                 install path: /home/brian/repos/medigator/venv/lib/python3.6/site-packages/pydantic
               python version: 3.6.9 (default, Oct  8 2020, 12:12:24)  [GCC 8.4.0]
                     platform: Linux-4.15.0-121-generic-x86_64-with-Ubuntu-18.04-bionic
     optional deps. installed: ['typing-extensions', 'email-validator']
```

The example code from the docs demonstrating the use of passing a config to `validate_arguments` causes an error when running mypy (version 0.790):

```py
from pydantic import ValidationError, validate_arguments


class Foobar:
    def __init__(self, v: str):
        self.v = v

    def __add__(self, other: 'Foobar') -> str:
        return f'{self} + {other}'

    def __str__(self) -> str:
        return f'Foobar({self.v})'


@validate_arguments(config=dict(arbitrary_types_allowed=True))
def add_foobars(a: Foobar, b: Foobar):
    return a + b
```

```
test.py:15: error: <nothing> not callable
```

If I remove the `config` parameter but still call `validate_arguments` it still occurs.
If I remove the call and just decorate with `@validate_arguments` then mypy is happy.

I assume it has something to do with the type annotation of `validate_arguments`.

Hints from issue discussion:

Commit message: Add change file

Patch to review:
diff --git a/changes/2055-layday.md b/changes/2055-layday.md
new file mode 100644
index 00000000000..026555d6b60
--- /dev/null
+++ b/changes/2055-layday.md
@@ -0,0 +1 @@
+add two overload variants to `validate_arguments` for the nested decorator signature
diff --git a/pydantic/decorator.py b/pydantic/decorator.py
index 589baaacac4..0d7ca8822a3 100644
--- a/pydantic/decorator.py
+++ b/pydantic/decorator.py
@@ -1,5 +1,19 @@
 from functools import wraps
-from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Tuple, Type, TypeVar, Union, cast, get_type_hints
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Callable,
+    Dict,
+    List,
+    Mapping,
+    Optional,
+    Tuple,
+    Type,
+    TypeVar,
+    Union,
+    get_type_hints,
+    overload,
+)
 
 from . import validator
 from .errors import ConfigError
@@ -11,16 +25,26 @@
 if TYPE_CHECKING:
     from .typing import AnyCallable
 
-    Callable = TypeVar('Callable', bound=AnyCallable)
+    AnyCallableT = TypeVar('AnyCallableT', bound=AnyCallable)
     ConfigType = Union[None, Type[Any], Dict[str, Any]]
 
 
-def validate_arguments(func: 'Callable' = None, *, config: 'ConfigType' = None) -> 'Callable':
+@overload
+def validate_arguments(func: None = None, *, config: 'ConfigType' = None) -> Callable[['AnyCallableT'], 'AnyCallableT']:
+    ...
+
+
+@overload
+def validate_arguments(func: 'AnyCallableT') -> 'AnyCallableT':
+    ...
+
+
+def validate_arguments(func: Optional['AnyCallableT'] = None, *, config: 'ConfigType' = None) -> Any:
     """
     Decorator to validate the arguments passed to a function.
     """
 
-    def validate(_func: 'Callable') -> 'Callable':
+    def validate(_func: 'AnyCallable') -> 'AnyCallable':
         vd = ValidatedFunction(_func, config)
 
         @wraps(_func)
@@ -30,12 +54,12 @@ def wrapper_function(*args: Any, **kwargs: Any) -> Any:
         wrapper_function.vd = vd  # type: ignore
         wrapper_function.raw_function = vd.raw_function  # type: ignore
         wrapper_function.model = vd.model  # type: ignore
-        return cast('Callable', wrapper_function)
+        return wrapper_function
 
     if func:
         return validate(func)
     else:
-        return cast('Callable', validate)
+        return validate
 
 
 ALT_V_ARGS = 'v__args'
@@ -44,7 +68,7 @@ def wrapper_function(*args: Any, **kwargs: Any) -> Any:
 
 
 class ValidatedFunction:
-    def __init__(self, function: 'Callable', config: 'ConfigType'):  # noqa C901
+    def __init__(self, function: 'AnyCallableT', config: 'ConfigType'):  # noqa C901
         from inspect import Parameter, signature
 
         parameters: Mapping[str, Parameter] = signature(function).parameters
diff --git a/tests/mypy/modules/fail4.py b/tests/mypy/modules/fail4.py
index f5d88fddbd5..be12a08c842 100644
--- a/tests/mypy/modules/fail4.py
+++ b/tests/mypy/modules/fail4.py
@@ -24,3 +24,13 @@ def bar() -> str:
 
 # return type should be a string
 y: int = bar()
+
+
+# nested decorator should not produce an error
+@validate_arguments(config={'arbitrary_types_allowed': True})
+def baz(a: int, *, c: str = 'x') -> str:
+    return c * a
+
+
+# ok
+z: str = baz(1, c='hello')


Code review comments:
diff_hunk: @@ -0,0 +1 @@
+add two overload variants to `validate_arguments` for the nested decorator signature
path: changes/2055-layday.md
text: @user1:
Maybe something more user oriented like "improve/fix typing of `validate_arguments` to be compliant with `mypy`"

@author:
Changed to:

> fix annotation of `validate_arguments` when passing configuration as argument

diff_hunk: @@ -24,3 +24,13 @@ def bar() -> str:
 
 # return type should be a string
 y: int = bar()
+
+
+# nested decorator should not produce an error
+@validate_arguments(config={'arbitrary_types_allowed': True})
path: tests/mypy/modules/fail4.py
text: @user1:
Why not put it in `success.py` instead of `fail4.py`?

@author:
done