Repository: samuelcolvin/pydantic (python)

Issue:
Add `arbitrary_types_allowed` to validate_arguments
# Feature Request

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.5a1
            pydantic compiled: True
                 install path: /home/xxx/.conda/envs/xxx/lib/python3.7/site-packages/pydantic
               python version: 3.7.4 (default, Aug 13 2019, 20:35:49)  [GCC 7.3.0]
                     platform: Linux-4.18.0-80.el8.x86_64-x86_64-with-centos-8.0.1905-Core
     optional deps. installed: ['typing-extensions', 'email-validator']
```
Hi, when I use the decorator `validate_arguments`, I have to define validate function for every user defined class. Is there a way to set `arbitrary_types_allowed` `True` or add to the arguments of `validate_arguments`?



Pull request:
Valdiate arguments config
## Change Summary

Work in progress, currently tests are failing as `fields` isn't working properly.

## Related issue number

This replaces #1378 which I merged prematurely.

Will fix #1376.

## 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
* [ ] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](ht tps://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)


Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
index 838581180..b87a10e70 100644
--- a/.github/ISSUE_TEMPLATE/bug.md
+++ b/.github/ISSUE_TEMPLATE/bug.md
@@ -4,6 +4,14 @@ about: Create a bug report to help us improve pydantic
 labels: bug
 ---
 
+### Checks
+
+* [ ] I added a descriptive title to this issue
+* [ ] I have searched (google, github) for similar issues and couldn't find anything
+* [ ] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug
+
+<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->
+
 # Bug
 
 Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index c643fafaf..5a43008e5 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -4,6 +4,18 @@ about: Suggest a new feature or change to pydantic
 labels: feature request
 ---
 
+### Checks
+
+* [ ] I added a descriptive title to this issue
+* [ ] I have searched (google, github) for similar issues and couldn't find anything
+* [ ] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this feature/change is needed
+* [ ] After submitting this, I commit to one of:
+    * Look through open issues and helped at least one other person
+    * Hit the "watch" button on this repo to receive notifications and I commit to help at least 2 people that ask questions in the future
+    * Implement a Pull Request for a confirmed bug
+
+<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->
+
 # Feature Request
 
 Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index 5618c8b8c..d30d3268c 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -4,6 +4,18 @@ about: Ask a question about how to use pydantic
 labels: question
 ---
 
+### Checks
+
+* [ ] I added a descriptive title to this issue
+* [ ] I have searched (google, github) for similar issues and couldn't find anything
+* [ ] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and couldn't find an answer
+* [ ] After submitting this, I commit to one of:
+    * Look through open issues and helped at least one other person
+    * Hit the "watch" button on this repo to receive notifications and I commit to help at least 2 people that ask questions in the future
+    * Implement a Pull Request for a confirmed bug
+
+<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->
+
 # Question
 
 Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
diff --git a/changes/1663-samuelcolvin.md b/changes/1663-samuelcolvin.md
new file mode 100644
index 000000000..1b3fec980
--- /dev/null
+++ b/changes/1663-samuelcolvin.md
@@ -0,0 +1,1 @@
+add `config` to `@validate_arguments`
diff --git a/docs/examples/validation_decorator_config.py b/docs/examples/validation_decorator_config.py
new file mode 100644
index 000000000..b25c7f956
--- /dev/null
+++ b/docs/examples/validation_decorator_config.py
@@ -0,0 +1,26 @@
+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
+
+
+c = add_foobars(Foobar('a'), Foobar('b'))
+print(c)
+
+try:
+    add_foobars(1, 2)
+except ValidationError as e:
+    print(e)
diff --git a/docs/usage/validation_decorator.md b/docs/usage/validation_decorator.md
index 9012134f8..3161d0719 100644
--- a/docs/usage/validation_decorator.md
+++ b/docs/usage/validation_decorator.md
@@ -78,8 +78,23 @@ _(This script is complete, it should run "as is")_
 ```py
 {!.tmp_examples/validation_decorator_async.py!}
 ```
-_(This script is complete, it should run "as is")_
 
+## Custom Config
+
+The model behind `validate_arguments` can be customised using a config setting which is equivalent to 
+setting the `Config` sub-class in normal models.
+
+!!! warning
+    The `fields` and `alias_generator` properties of `Config` which allow aliases to be configured are not supported
+    yet with `@validate_arguments`, using them will raise an error.
+    
+Configuration is set using the `config` keyword argument to the decorator, it may be either a config class
+or a dict of properties which are converted to a class later.
+
+```py
+{!.tmp_examples/validation_decorator_config.py!}
+```
+_(This script is complete, it should run "as is")_
 
 ## Limitations
 
@@ -126,7 +141,9 @@ in future.
 
 ### Config and Validators
 
-Custom [`Config`](model_config.md) and [validators](validators.md) are not yet supported.
+`fields` and `alias_generator` on custom [`Config`](model_config.md) are not supported, see [above](#custom-config).
+ 
+Neither are [validators](validators.md).
 
 ### Model fields and reserved arguments
 
diff --git a/pydantic/decorator.py b/pydantic/decorator.py
index 133326ca1..589baaaca 100644
--- a/pydantic/decorator.py
+++ b/pydantic/decorator.py
@@ -1,5 +1,5 @@
 from functools import wraps
-from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Tuple, TypeVar, cast, get_type_hints
+from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Tuple, Type, TypeVar, Union, cast, get_type_hints
 
 from . import validator
 from .errors import ConfigError
@@ -12,22 +12,30 @@ if TYPE_CHECKING:
     from .typing import AnyCallable
 
     Callable = TypeVar('Callable', bound=AnyCallable)
+    ConfigType = Union[None, Type[Any], Dict[str, Any]]
 
 
-def validate_arguments(function: 'Callable') -> 'Callable':
+def validate_arguments(func: 'Callable' = None, *, config: 'ConfigType' = None) -> 'Callable':
     """
     Decorator to validate the arguments passed to a function.
     """
-    vd = ValidatedFunction(function)
 
-    @wraps(function)
-    def wrapper_function(*args: Any, **kwargs: Any) -> Any:
-        return vd.call(*args, **kwargs)
+    def validate(_func: 'Callable') -> 'Callable':
+        vd = ValidatedFunction(_func, config)
 
-    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)
+        @wraps(_func)
+        def wrapper_function(*args: Any, **kwargs: Any) -> Any:
+            return vd.call(*args, **kwargs)
+
+        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)
+
+    if func:
+        return validate(func)
+    else:
+        return cast('Callable', validate)
 
 
 ALT_V_ARGS = 'v__args'
@@ -36,7 +44,7 @@ V_POSITIONAL_ONLY_NAME = 'v__positional_only'
 
 
 class ValidatedFunction:
-    def __init__(self, function: 'Callable'):
+    def __init__(self, function: 'Callable', config: 'ConfigType'):  # noqa C901
         from inspect import Parameter, signature
 
         parameters: Mapping[str, Parameter] = signature(function).parameters
@@ -100,7 +108,7 @@ class ValidatedFunction:
             # same with kwargs
             fields[self.v_kwargs_name] = Dict[Any, Any], None
 
-        self.create_model(fields, takes_args, takes_kwargs)
+        self.create_model(fields, takes_args, takes_kwargs, config)
 
     def call(self, *args: Any, **kwargs: Any) -> Any:
         values = self.build_values(args, kwargs)
@@ -170,9 +178,24 @@ class ValidatedFunction:
         else:
             return self.raw_function(**d)
 
-    def create_model(self, fields: Dict[str, Any], takes_args: bool, takes_kwargs: bool) -> None:
+    def create_model(self, fields: Dict[str, Any], takes_args: bool, takes_kwargs: bool, config: 'ConfigType') -> None:
         pos_args = len(self.arg_mapping)
 
+        class CustomConfig:
+            pass
+
+        if not TYPE_CHECKING:  # pragma: no branch
+            if isinstance(config, dict):
+                CustomConfig = type('Config', (), config)  # noqa: F811
+            elif config is not None:
+                CustomConfig = config  # noqa: F811
+
+        if hasattr(CustomConfig, 'fields') or hasattr(CustomConfig, 'alias_generator'):
+            raise ConfigError(
+                'Setting the "fields" and "alias_generator" property on custom Config for '
+                '@validate_arguments is not yet supported, please remove.'
+            )
+
         class DecoratorBaseModel(BaseModel):
             @validator(self.v_args_name, check_fields=False, allow_reuse=True)
             def check_args(cls, v: List[Any]) -> List[Any]:
@@ -196,7 +219,7 @@ class ValidatedFunction:
                 keys = ', '.join(map(repr, v))
                 raise TypeError(f'positional-only argument{plural} passed as keyword argument{plural}: {keys}')
 
-            class Config:
+            class Config(CustomConfig):
                 extra = Extra.forbid
 
         self.model = create_model(to_camel(self.raw_function.__name__), __base__=DecoratorBaseModel, **fields)
diff --git a/setup.cfg b/setup.cfg
index 621c2b2ed..732c297e2 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,5 @@
 [tool:pytest]
 testpaths = tests
-timeout = 10
 filterwarnings =
     error
     ignore::DeprecationWarning:distutils


Test patch:
diff --git a/tests/test_decorator.py b/tests/test_decorator.py
index 472796177..75928f27f 100644
--- a/tests/test_decorator.py
+++ b/tests/test_decorator.py
@@ -70,6 +70,7 @@ def test_wrap():
     assert issubclass(foo_bar.model, BaseModel)
     assert foo_bar.model.__fields__.keys() == {'a', 'b', 'args', 'kwargs'}
     assert foo_bar.model.__name__ == 'FooBar'
+    assert foo_bar.model.schema()['title'] == 'FooBar'
     # signature is slightly different on 3.6
     if sys.version_info >= (3, 7):
         assert repr(inspect.signature(foo_bar)) == '<Signature (a: int, b: int)>'
@@ -262,3 +263,57 @@ def test_class_method():
         {'loc': ('a',), 'msg': 'field required', 'type': 'value_error.missing'},
         {'loc': ('b',), 'msg': 'field required', 'type': 'value_error.missing'},
     ]
+
+
+def test_config_title():
+    @validate_arguments(config=dict(title='Testing'))
+    def foo(a: int, b: int):
+        return f'{a}, {b}'
+
+    assert foo(1, 2) == '1, 2'
+    assert foo(1, b=2) == '1, 2'
+    assert foo.model.schema()['title'] == 'Testing'
+
+
+def test_config_title_cls():
+    class Config:
+        title = 'Testing'
+
+    @validate_arguments(config=Config)
+    def foo(a: int, b: int):
+        return f'{a}, {b}'
+
+    assert foo(1, 2) == '1, 2'
+    assert foo(1, b=2) == '1, 2'
+    assert foo.model.schema()['title'] == 'Testing'
+
+
+def test_config_fields():
+    with pytest.raises(ConfigError, match='Setting the "fields" and "alias_generator" property on custom Config for @'):
+
+        @validate_arguments(config=dict(fields={'b': 'bang'}))
+        def foo(a: int, b: int):
+            return f'{a}, {b}'
+
+
+def test_config_arbitrary_types_allowed():
+    class EggBox:
+        def __str__(self) -> str:
+            return 'EggBox()'
+
+    @validate_arguments(config=dict(arbitrary_types_allowed=True))
+    def foo(a: int, b: EggBox):
+        return f'{a}, {b}'
+
+    assert foo(1, EggBox()) == '1, EggBox()'
+    with pytest.raises(ValidationError) as exc_info:
+        assert foo(1, 2) == '1, 2'
+
+    assert exc_info.value.errors() == [
+        {
+            'loc': ('b',),
+            'msg': 'instance of EggBox expected',
+            'type': 'type_error.arbitrary_type',
+            'ctx': {'expected_arbitrary_type': 'EggBox'},
+        },
+    ]